From e4f9c25f81ae53c900d505632c9e827a9eb66122 Mon Sep 17 00:00:00 2001 From: Jennifer Eng Date: Mon, 1 Nov 2021 16:20:54 -0700 Subject: [PATCH] add code --- Collagen_Bx2-4.ipynb | 506 +++++++ GateCellTypes.ipynb | 573 ++++++++ Normalize_Bx2-4.ipynb | 1198 ++++++++++++++++ mplex_image/20210312_visualize.py | 288 ++++ mplex_image/__init__.py | 0 .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 168 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 172 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 193 bytes .../__pycache__/analyze.cpython-37.pyc | Bin 0 -> 8381 bytes .../__pycache__/analyze.cpython-38.pyc | Bin 0 -> 8173 bytes .../__pycache__/analyze.cpython-39.pyc | Bin 0 -> 8252 bytes mplex_image/__pycache__/cmif.cpython-37.pyc | Bin 0 -> 25835 bytes mplex_image/__pycache__/cmif.cpython-38.pyc | Bin 0 -> 25849 bytes mplex_image/__pycache__/cmif.cpython-39.pyc | Bin 0 -> 25759 bytes mplex_image/__pycache__/codex.cpython-37.pyc | Bin 0 -> 21956 bytes mplex_image/__pycache__/codex.cpython-38.pyc | Bin 0 -> 22289 bytes .../__pycache__/features.cpython-37.pyc | Bin 0 -> 19629 bytes .../__pycache__/features.cpython-38.pyc | Bin 0 -> 21150 bytes .../__pycache__/features.cpython-39.pyc | Bin 0 -> 21115 bytes mplex_image/__pycache__/gating.cpython-38.pyc | Bin 0 -> 6923 bytes mplex_image/__pycache__/gating.cpython-39.pyc | Bin 0 -> 6979 bytes .../__pycache__/getdata.cpython-37.pyc | Bin 0 -> 4295 bytes .../__pycache__/getdata.cpython-38.pyc | Bin 0 -> 4284 bytes .../__pycache__/getdata.cpython-39.pyc | Bin 0 -> 4324 bytes .../__pycache__/imagine.cpython-37.pyc | Bin 0 -> 11584 bytes .../__pycache__/imagine.cpython-38.pyc | Bin 0 -> 11540 bytes .../__pycache__/metadata.cpython-37.pyc | Bin 0 -> 5256 bytes .../__pycache__/metadata.cpython-38.pyc | Bin 0 -> 5115 bytes mplex_image/__pycache__/mics.cpython-38.pyc | Bin 0 -> 28139 bytes mplex_image/__pycache__/mics.cpython-39.pyc | Bin 0 -> 27977 bytes .../__pycache__/mpimage.cpython-37.pyc | Bin 0 -> 27628 bytes .../__pycache__/mpimage.cpython-38.pyc | Bin 0 -> 27625 bytes .../__pycache__/mpimage.cpython-39.pyc | Bin 0 -> 27503 bytes .../__pycache__/normalize.cpython-38.pyc | Bin 0 -> 19121 bytes .../__pycache__/normalize.cpython-39.pyc | Bin 0 -> 18943 bytes .../__pycache__/ometiff.cpython-37.pyc | Bin 0 -> 1575 bytes .../__pycache__/ometiff.cpython-38.pyc | Bin 0 -> 1575 bytes .../__pycache__/ometiff.cpython-39.pyc | Bin 0 -> 1559 bytes .../__pycache__/preprocess.cpython-37.pyc | Bin 0 -> 23888 bytes .../__pycache__/preprocess.cpython-38.pyc | Bin 0 -> 23774 bytes .../__pycache__/preprocess.cpython-39.pyc | Bin 0 -> 23111 bytes .../__pycache__/process.cpython-37.pyc | Bin 0 -> 41042 bytes .../__pycache__/process.cpython-38.pyc | Bin 0 -> 43367 bytes .../__pycache__/process.cpython-39.pyc | Bin 0 -> 42755 bytes .../__pycache__/register.cpython-37.pyc | Bin 0 -> 4509 bytes .../__pycache__/register.cpython-38.pyc | Bin 0 -> 4522 bytes .../__pycache__/register.cpython-39.pyc | Bin 0 -> 4484 bytes .../__pycache__/segment.cpython-37.pyc | Bin 0 -> 21724 bytes .../__pycache__/segment.cpython-38.pyc | Bin 0 -> 21145 bytes .../__pycache__/segment.cpython-39.pyc | Bin 0 -> 21335 bytes .../__pycache__/visualize.cpython-37.pyc | Bin 0 -> 8994 bytes .../__pycache__/visualize.cpython-38.pyc | Bin 0 -> 12984 bytes .../__pycache__/visualize.cpython-39.pyc | Bin 0 -> 13026 bytes mplex_image/_version.py | 1 + mplex_image/analyze.py | 300 ++++ mplex_image/cmif.py | 705 ++++++++++ mplex_image/codex.py | 452 ++++++ mplex_image/features.py | 603 ++++++++ mplex_image/gating.py | 205 +++ mplex_image/getdata.py | 176 +++ mplex_image/imagine.py | 504 +++++++ mplex_image/metadata.py | 176 +++ mplex_image/mics.py | 581 ++++++++ mplex_image/mpimage.py | 817 +++++++++++ mplex_image/normalize.py | 536 ++++++++ mplex_image/ometiff.py | 76 ++ mplex_image/preprocess.py | 705 ++++++++++ mplex_image/process.py | 1208 +++++++++++++++++ mplex_image/register.py | 105 ++ mplex_image/segment.py | 717 ++++++++++ mplex_image/visualize.py | 387 ++++++ 71 files changed, 10819 insertions(+) create mode 100755 Collagen_Bx2-4.ipynb create mode 100755 GateCellTypes.ipynb create mode 100755 Normalize_Bx2-4.ipynb create mode 100755 mplex_image/20210312_visualize.py create mode 100755 mplex_image/__init__.py create mode 100755 mplex_image/__pycache__/__init__.cpython-37.pyc create mode 100755 mplex_image/__pycache__/__init__.cpython-38.pyc create mode 100644 mplex_image/__pycache__/__init__.cpython-39.pyc create mode 100755 mplex_image/__pycache__/analyze.cpython-37.pyc create mode 100755 mplex_image/__pycache__/analyze.cpython-38.pyc create mode 100644 mplex_image/__pycache__/analyze.cpython-39.pyc create mode 100755 mplex_image/__pycache__/cmif.cpython-37.pyc create mode 100755 mplex_image/__pycache__/cmif.cpython-38.pyc create mode 100755 mplex_image/__pycache__/cmif.cpython-39.pyc create mode 100755 mplex_image/__pycache__/codex.cpython-37.pyc create mode 100755 mplex_image/__pycache__/codex.cpython-38.pyc create mode 100755 mplex_image/__pycache__/features.cpython-37.pyc create mode 100755 mplex_image/__pycache__/features.cpython-38.pyc create mode 100755 mplex_image/__pycache__/features.cpython-39.pyc create mode 100755 mplex_image/__pycache__/gating.cpython-38.pyc create mode 100644 mplex_image/__pycache__/gating.cpython-39.pyc create mode 100755 mplex_image/__pycache__/getdata.cpython-37.pyc create mode 100755 mplex_image/__pycache__/getdata.cpython-38.pyc create mode 100755 mplex_image/__pycache__/getdata.cpython-39.pyc create mode 100755 mplex_image/__pycache__/imagine.cpython-37.pyc create mode 100755 mplex_image/__pycache__/imagine.cpython-38.pyc create mode 100755 mplex_image/__pycache__/metadata.cpython-37.pyc create mode 100755 mplex_image/__pycache__/metadata.cpython-38.pyc create mode 100755 mplex_image/__pycache__/mics.cpython-38.pyc create mode 100755 mplex_image/__pycache__/mics.cpython-39.pyc create mode 100755 mplex_image/__pycache__/mpimage.cpython-37.pyc create mode 100755 mplex_image/__pycache__/mpimage.cpython-38.pyc create mode 100755 mplex_image/__pycache__/mpimage.cpython-39.pyc create mode 100755 mplex_image/__pycache__/normalize.cpython-38.pyc create mode 100755 mplex_image/__pycache__/normalize.cpython-39.pyc create mode 100755 mplex_image/__pycache__/ometiff.cpython-37.pyc create mode 100755 mplex_image/__pycache__/ometiff.cpython-38.pyc create mode 100755 mplex_image/__pycache__/ometiff.cpython-39.pyc create mode 100755 mplex_image/__pycache__/preprocess.cpython-37.pyc create mode 100755 mplex_image/__pycache__/preprocess.cpython-38.pyc create mode 100755 mplex_image/__pycache__/preprocess.cpython-39.pyc create mode 100755 mplex_image/__pycache__/process.cpython-37.pyc create mode 100755 mplex_image/__pycache__/process.cpython-38.pyc create mode 100755 mplex_image/__pycache__/process.cpython-39.pyc create mode 100755 mplex_image/__pycache__/register.cpython-37.pyc create mode 100755 mplex_image/__pycache__/register.cpython-38.pyc create mode 100755 mplex_image/__pycache__/register.cpython-39.pyc create mode 100755 mplex_image/__pycache__/segment.cpython-37.pyc create mode 100755 mplex_image/__pycache__/segment.cpython-38.pyc create mode 100755 mplex_image/__pycache__/segment.cpython-39.pyc create mode 100755 mplex_image/__pycache__/visualize.cpython-37.pyc create mode 100755 mplex_image/__pycache__/visualize.cpython-38.pyc create mode 100755 mplex_image/__pycache__/visualize.cpython-39.pyc create mode 100755 mplex_image/_version.py create mode 100755 mplex_image/analyze.py create mode 100755 mplex_image/cmif.py create mode 100755 mplex_image/codex.py create mode 100755 mplex_image/features.py create mode 100755 mplex_image/gating.py create mode 100755 mplex_image/getdata.py create mode 100755 mplex_image/imagine.py create mode 100755 mplex_image/metadata.py create mode 100755 mplex_image/mics.py create mode 100755 mplex_image/mpimage.py create mode 100755 mplex_image/normalize.py create mode 100755 mplex_image/ometiff.py create mode 100755 mplex_image/preprocess.py create mode 100755 mplex_image/process.py create mode 100755 mplex_image/register.py create mode 100755 mplex_image/segment.py create mode 100755 mplex_image/visualize.py diff --git a/Collagen_Bx2-4.ipynb b/Collagen_Bx2-4.ipynb new file mode 100755 index 0000000..a30d41c --- /dev/null +++ b/Collagen_Bx2-4.ipynb @@ -0,0 +1,506 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#load libraries\n", + "\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import os\n", + "import copy\n", + "import seaborn as sns\n", + "import importlib\n", + "import scipy\n", + "\n", + "import scanpy as sc\n", + "from sklearn.cluster import KMeans\n", + "from sklearn.preprocessing import scale, minmax_scale\n", + "from sklearn.metrics import silhouette_score\n", + "import matplotlib as mpl\n", + "mpl.rc('figure', max_open_warning = 0)\n", + "#mpl.font_manager._rebuild()\n", + "mpl.rcParams['mathtext.fontset'] = 'custom'\n", + "mpl.rcParams['mathtext.it'] = 'Arial:italic'\n", + "mpl.rcParams['mathtext.rm'] = 'Arial'\n", + "mpl.rcParams['font.sans-serif'] = \"Arial\"\n", + "mpl.rcParams['font.family'] = \"sans-serif\"\n", + "mpl.rc('font', serif='Arial') \n", + "codedir = os.getcwd()\n", + "#load cmif libraries\n", + "#os.chdir('/home/groups/graylab_share/OMERO.rdsStore/engje/Data/cmIF')\n", + "from mplex_image import visualize as viz, process, preprocess, normalize" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os.chdir(codedir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "np.random.seed(222)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Table of contents \n", + "1. [Load Data](#load)\n", + "2. [Normalize](#norm)\n", + "6. [Visualize Normalization](#normviz)\n", + "[leiden for cell typing](#clusterlei)\n", + "7. [Cluster K means](#cluster)\n", + "8. [Leiden cluster](#clust1)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#load data\n", + "os.chdir(f'{codedir}/paper_data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "s_date = '20210402'\n", + "if not os.path.exists(s_date):\n", + " os.mkdir(s_date)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Load Data \n", + "\n", + "2.\tAs Ki67 is not continuous antigen, can you count positive cells (Proliferative cluster) by distance (<25, 25-50, 50-75, >75) from collagen I in each Bx?\n", + "\n", + "3.\tCould you map cells by distance (<25, 25-50, 50-75, >75) from collagen I in each Bx? If you can add a distance column (1-4) in the cluster csv, I can make it in Qi.\n", + "\n", + "4.\tCould you try to see the correlation between ER/PCNA and (VIM+aSMA+CD31)? – not necessary to show significance. (see attached image from Bx1 Scene-003)\n", + "\n", + "[contents](#contents)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### not normalized" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_mi = pd.read_csv('20210324_SMTBx1-4_JE-TMA-43_60_62_FilteredMeanIntensity.csv',index_col=0) \n", + "df_mi['slide'] = [item.split('_')[0] for item in df_mi.index]\n", + "df_mi['slide_scene'] = [item.split('_cell')[0] for item in df_mi.index]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for s_file in os.listdir():\n", + " if s_file.find('MaskDistances') > -1:\n", + " print(s_file)\n", + "df_mask = pd.DataFrame()\n", + "for s_sample in ['SMT101Bx1-16','SMTBx2-5','SMTBx3','SMTBx4-3','HTA-33']: #'SMT101Bx4-3',\n", + " df_mask = df_mask.append(pd.read_csv(f'features_{s_sample}_MaskDistances.csv',index_col=0))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_mask.columns\n", + "ls_target = ['Vim_dist','CD31_dist', 'PDPN_dist', 'aSMA_dist', 'CD68_dist','ColI_dist', 'ColIV_dist']\n", + "ls_marker = ['ER_nuclei','Ki67_nuclei','PCNA_nuclei']\n", + "ls_drop = ['HTA-33_scene001','SMTBx1-16_scene001'#,'SMT101Bx4-3_scene001','SMT101Bx4-3_scene002'\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = df_mi.merge(df_mask.loc[:,ls_target],left_index=True,right_index=True)\n", + "df = df[(~df.Vim_dist.isna()) & (~df.slide_scene.isin(ls_drop))]\n", + "df.loc[:,ls_target] = df.loc[:,ls_target]*.325" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "#fit\n", + "data = df.loc[:,ls_marker].T\n", + "batch = df.slide\n", + "bayesdata = normalize.combat(data, batch)\n", + "df_norm = bayesdata.T" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_norm['slide'] = df.slide\n", + "df_norm.groupby('slide').mean()\n", + "df_norm.groupby('slide').std()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df['Vim-CD31-aSMA_dist'] = df.loc[:,['Vim_dist','CD31_dist','aSMA_dist']].min(axis=1)\n", + "ls_target = ls_target + ['Vim-CD31-aSMA_dist']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "mpl.rcParams['pdf.fonttype'] = 42\n", + "mpl.rcParams['ps.fonttype'] = 42\n", + "%matplotlib inline\n", + "#by tissue no Bx1\n", + "sns.set(style='white')\n", + "import matplotlib.ticker as tic\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "tot = 0\n", + "ls_dist = [25, 50, 75]\n", + "i_diff = 25\n", + "ls_slide = ['SMTBx2-5', 'SMTBx3','SMT1Bx4-3'] #'\n", + "d_slide = {'SMTBx1-16':'Bx1', 'SMTBx2-5':'Bx2', 'SMTBx3':'Bx3','HTA-33':'Bx4-HTAN','SMTBx4-3':'Bx4'}\n", + "for s_target in ['ColI_dist', 'ColIV_dist','Vim-CD31-aSMA_dist']:\n", + " print(s_target)\n", + " fig, ax = plt.subplots(3,2, figsize=(4.5,4),sharex=True,dpi=300)\n", + " for idxc, s_slide in enumerate(ls_slide):\n", + " print(s_slide)\n", + " df_slide = df[df.slide==s_slide]\n", + " for idx, s_marker in enumerate(['ER_nuclei', 'PCNA_nuclei']): #,'Ki67_nuclei']):\n", + " print(s_marker)\n", + " df_result = pd.DataFrame(index=df_slide.index)\n", + " for s_dist in ls_dist:\n", + " b_bool = (df_slide.loc[:,s_target] < s_dist) & (df_slide.loc[:,s_target] >= s_dist - i_diff)\n", + " df_result.loc[b_bool,f'{s_marker}_{s_dist}'] = df_slide.loc[b_bool,s_marker]\n", + " for s_col in df_result.columns:\n", + " sns.kdeplot(df_result.loc[:,s_col].dropna(), ax=ax[idxc,idx],\n", + " label=f\"< {s_col.split('_')[2]}\"#,fill=True, alpha=0.3\n", + " )\n", + " if df_result.mean().fillna(0)[2] == 0:\n", + " statistic, pvalue = scipy.stats.f_oneway(df_result.iloc[:,0].dropna(),df_result.iloc[:,1].dropna())\n", + " print(len(df_result.iloc[:,0].dropna()))\n", + " print(len(df_result.iloc[:,1].dropna()))\n", + " else:\n", + " statistic, pvalue = scipy.stats.f_oneway(df_result.iloc[:,0].dropna(),df_result.iloc[:,1].dropna(),df_result.iloc[:,2].dropna())\n", + " print(len(df_result.iloc[:,0].dropna()))\n", + " print(len(df_result.iloc[:,1].dropna()))\n", + " print('over75')\n", + " print(len(df_result.iloc[:,2].dropna()))\n", + " ax[idxc,idx].set_xlabel(f\"{s_col.split('_')[0]} Intensity\",fontname=\"Arial\",fontsize=18)\n", + " ax[idxc,idx].set_ylabel(f\"\")\n", + " ax[idxc,idx].set_title(f\"\")\n", + " temp = tic.MaxNLocator(3)\n", + " ax[idxc,idx].set_yticklabels(())\n", + " ax[idxc,idx].xaxis.set_major_locator(temp)\n", + " tot+=1\n", + " if pvalue < 0.001: # 0.05/30: #bonferoni correction\n", + " ax[idxc,idx].text(0.42, 0.87, '*',\n", + " horizontalalignment='center',\n", + " verticalalignment='center',\n", + " transform=ax[idxc,idx].transAxes)\n", + " ax[idxc,idx].set_xlim(-1000,5500)\n", + " ax[idxc,idx].spines['right'].set_visible(False)\n", + " ax[idxc,idx].spines['left'].set_visible(False)\n", + " ax[idxc,idx].spines['top'].set_visible(False)\n", + " #print(ax[idxc,idx].get_xticklabels())\n", + " #ax[idxc,idx].set_xticklabels(ax[idxc,idx].get_xticklabels(),{'fontsize':16})\n", + " ax[idxc,0].set_ylabel(f\"{d_slide[s_slide]}\",fontname=\"Arial\",fontsize=18)\n", + " ax[2,1].legend(title='$\\mu$m',borderpad=.3,labelspacing=.3,loc=4,fontsize=14)\n", + " plt.subplots_adjust(wspace=.001,hspace=.001)\n", + " plt.suptitle(f\"Distance to {s_target.split('_')[0]}\",y=.93,fontname=\"Arial\",fontsize=24)\n", + " plt.tight_layout()\n", + " fig.savefig(f'./{s_date}/IntensityvsDistance_{i_diff}s_{s_target}_by_slide_noBx1.png',dpi=300)\n", + " #fig.savefig(f'./{s_date}/IntensityvsDistance_{i_diff}s_{s_target}_by_slide_noBx1.pdf',dpi=200)\n", + " #break" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + " 0.05/30" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "from matplotlib import gridspec\n", + "ax_objs = []\n", + "ls_slide = ['SMTBx2-5', 'SMTBx3','SMT1Bx4-3'] #'\n", + "d_slide = {'SMTBx1-16':'Bx1', 'SMTBx2-5':'Bx2', 'SMTBx3':'Bx3','HTA-33':'Bx4-HTAN','SMTBx4-3':'Bx4'}\n", + "for s_target in ['ColI_dist', 'ColIV_dist','Vim-CD31-aSMA_dist']:\n", + " fig = plt.figure(figsize=(5.5,3.5),dpi=300)\n", + " gs = gridspec.GridSpec(nrows=3, ncols=2,figure=fig, \n", + " wspace=0.1, hspace=0.05,left=0.1, right=.75\n", + " )\n", + " for idxc, s_slide in enumerate(ls_slide):\n", + " df_slide = df[df.slide==s_slide]\n", + " for idx, s_marker in enumerate(['ER_nuclei', 'PCNA_nuclei']):\n", + " ax_objs.append(fig.add_subplot(gs[idxc,idx]))\n", + " df_result = pd.DataFrame(index=df_slide.index)\n", + " for s_dist in ls_dist:\n", + " b_bool = (df_slide.loc[:,s_target] < s_dist) & (df_slide.loc[:,s_target] >= s_dist - i_diff)\n", + " df_result.loc[b_bool,f'{s_marker}_{s_dist}'] = df_slide.loc[b_bool,s_marker]\n", + " for s_col in df_result.columns:\n", + " g =sns.kdeplot(df_result.loc[:,s_col].dropna(), ax=ax_objs[-1],\n", + " label=f\"< {s_col.split('_')[2]}\"#,fill=True,alpha=0.5\n", + " )\n", + " if df_result.mean().fillna(0)[2] == 0:\n", + " statistic, pvalue = scipy.stats.f_oneway(df_result.iloc[:,0].dropna(),df_result.iloc[:,1].dropna())\n", + " #print(pvalue)\n", + " else:\n", + " statistic, pvalue = scipy.stats.f_oneway(df_result.iloc[:,0].dropna(),df_result.iloc[:,1].dropna(),df_result.iloc[:,2].dropna())\n", + " ax_objs[-1].set_ylabel(f\"\")\n", + " ax_objs[-1].set_title(f\"\")\n", + " temp = tic.MaxNLocator(3)\n", + " ax_objs[-1].set_yticklabels(())\n", + " ax_objs[-1].xaxis.set_major_locator(temp)\n", + " tot+=1\n", + " if pvalue < 0.001: # 0.05/30: #bonferoni correction\n", + " ax_objs[-1].text(0.55, 0.65, '*',\n", + " horizontalalignment='center',\n", + " verticalalignment='center',\n", + " transform=ax_objs[-1].transAxes)\n", + " ax_objs[-1].set_xlim(-1000,5500)\n", + " ax_objs[-1].spines['right'].set_visible(False)\n", + " ax_objs[-1].spines['left'].set_visible(False)\n", + " ax_objs[-1].spines['top'].set_visible(False)\n", + " #ax_objs[-1].spines['bottom'].set_visible(False)\n", + " ax_objs[-1].set_xlabel('')\n", + " rect = ax_objs[-1].patch\n", + " rect.set_alpha(0)\n", + " if idx == 0:\n", + " ax_objs[-1].set_ylabel(f\"{d_slide[s_slide]}\",fontsize=18)\n", + " if idx==1:\n", + " if idxc == 2:\n", + " ax_objs[-1].legend(title='$\\mu$m',borderpad=.3,labelspacing=.3,fontsize=12,loc='upper left', bbox_to_anchor=(1.05, 1.5))\n", + " if idxc ==2:\n", + " ax_objs[-1].set_xlabel(f\"{s_col.split('_')[0]} Intensity\",fontsize=18)\n", + " else:\n", + " ax_objs[-1].set_xticklabels([]) \n", + " plt.suptitle(f\"Distance to {s_target.split('_')[0]}\",x=.45,y=.95,fontsize=20)\n", + " gs.update(bottom = 0.2)\n", + " fig.savefig(f'./{s_date}/IntensityvsDistance_{i_diff}s_{s_target}_by_slide_noBx1_bigger.png',dpi=200)\n", + " #break" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#by tissue w bx1\n", + "%matplotlib inline\n", + "sns.set(style='white')\n", + "import matplotlib.ticker as tic\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "tot = 0\n", + "ls_dist = [25, 50, 75]\n", + "i_diff = 25\n", + "ls_slide = ['SMTBx1-16','SMTBx2-5', 'SMTBx3','SMT1Bx4-3'] #'\n", + "d_slide = {'SMTBx1-16':'Bx1', 'SMTBx2-5':'Bx2', 'SMTBx3':'Bx3','HTA-33':'Bx4-HTAN','SMTBx4-3':'Bx4'}\n", + "for s_target in ls_target + ['Vim-CD31-aSMA_dist']: #['CD68_dist','ColI_dist', 'ColIV_dist']:\n", + " fig, ax = plt.subplots(4,3, figsize=(7,5),sharex=True,dpi=300)\n", + " for idxc, s_slide in enumerate(ls_slide):\n", + " df_slide = df[df.slide==s_slide]\n", + " for idx, s_marker in enumerate(ls_marker):\n", + " df_result = pd.DataFrame(index=df_slide.index)\n", + " for s_dist in ls_dist:\n", + " b_bool = (df_slide.loc[:,s_target] < s_dist) & (df_slide.loc[:,s_target] >= s_dist - i_diff)\n", + " df_result.loc[b_bool,f'{s_marker}_{s_dist}'] = df_slide.loc[b_bool,s_marker]\n", + " for s_col in df_result.columns:\n", + " sns.kdeplot(df_result.loc[:,s_col].dropna(), ax=ax[idxc,idx], label=f\"< {s_col.split('_')[2]}\")\n", + " if df_result.mean().fillna(0)[2] == 0:\n", + " statistic, pvalue = scipy.stats.f_oneway(df_result.iloc[:,0].dropna(),df_result.iloc[:,1].dropna())\n", + " #print(pvalue)\n", + " else:\n", + " statistic, pvalue = scipy.stats.f_oneway(df_result.iloc[:,0].dropna(),df_result.iloc[:,1].dropna(),df_result.iloc[:,2].dropna())\n", + " ax[idxc,idx].set_xlabel(f\"{s_col.split('_')[0]} Intensity\",fontsize=18)\n", + " ax[idxc,idx].set_ylabel(f\"\")\n", + " ax[idxc,idx].set_title(f\"\")\n", + " temp = tic.MaxNLocator(3)\n", + " ax[idxc,idx].set_yticklabels(())\n", + " ax[idxc,idx].xaxis.set_major_locator(temp)\n", + " tot+=1\n", + " if pvalue < 0.001: # 0.05/30: #bonferoni correction\n", + " ax[idxc,idx].text(0.5, 0.8, '*',\n", + " horizontalalignment='center',\n", + " verticalalignment='center',\n", + " transform=ax[idxc,idx].transAxes)\n", + " ax[idxc,idx].set_xlim(-1500,7000)\n", + " ax[idxc,idx].spines['right'].set_visible(False)\n", + " ax[idxc,idx].spines['left'].set_visible(False)\n", + " ax[idxc,idx].spines['top'].set_visible(False)\n", + " ax[idxc,0].set_ylabel(f\"{d_slide[s_slide]}\",fontsize=18)\n", + " ax[0,2].legend(title='$\\mu$m')\n", + " plt.subplots_adjust(wspace=.001,hspace=.001)\n", + " plt.suptitle(f\"Distance to {s_target.split('_')[0]}\",fontsize=20)\n", + " plt.tight_layout()\n", + " fig.savefig(f'./{s_date}/IntensityvsDistance_25s_{s_target}_by_slide.png',dpi=300)\n", + " #break" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#by tissue w bx1\n", + "%matplotlib inline\n", + "sns.set(style='white')\n", + "import matplotlib.ticker as tic\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "tot = 0\n", + "ls_dist = [25, 50, 75]\n", + "i_diff = 25\n", + "ls_slide = ['SMTBx2-5', 'SMTBx3','SMT1Bx4-3'] #'SMTBx1-16',\n", + "d_slide = {'SMTBx1-16':'Bx1', 'SMTBx2-5':'Bx2', 'SMTBx3':'Bx3','HTA-33':'Bx4-HTAN','SMTBx4-3':'Bx4'}\n", + "for s_target in ['ColI_dist', 'ColIV_dist']:\n", + " fig, ax = plt.subplots(3,3, figsize=(7,4),sharex=True)\n", + " for idxc, s_slide in enumerate(ls_slide):\n", + " df_slide = df[df.slide==s_slide]\n", + " for idx, s_marker in enumerate(ls_marker):\n", + " df_result = pd.DataFrame(index=df_slide.index)\n", + " for s_dist in ls_dist:\n", + " b_bool = (df_slide.loc[:,s_target] < s_dist) & (df_slide.loc[:,s_target] >= s_dist - i_diff)\n", + " df_result.loc[b_bool,f'{s_marker}_{s_dist}'] = df_slide.loc[b_bool,s_marker]\n", + " for s_col in df_result.columns:\n", + " sns.kdeplot(df_result.loc[:,s_col].dropna(), ax=ax[idxc,idx], label=f\"< {s_col.split('_')[2]}\")\n", + " if df_result.mean().fillna(0)[2] == 0:\n", + " statistic, pvalue = scipy.stats.f_oneway(df_result.iloc[:,0].dropna(),df_result.iloc[:,1].dropna())\n", + " #print(pvalue)\n", + " else:\n", + " statistic, pvalue = scipy.stats.f_oneway(df_result.iloc[:,0].dropna(),df_result.iloc[:,1].dropna(),df_result.iloc[:,2].dropna())\n", + " ax[idxc,idx].set_xlabel(f\"{s_col.split('_')[0]} Intensity\")\n", + " ax[idxc,idx].set_ylabel(f\"\")\n", + " ax[idxc,idx].set_title(f\"\")\n", + " temp = tic.MaxNLocator(3)\n", + " ax[idxc,idx].set_yticklabels(())\n", + " ax[idxc,idx].xaxis.set_major_locator(temp)\n", + " tot+=1\n", + " if pvalue < 0.001: # 0.05/30: #bonferoni correction\n", + " ax[idxc,idx].text(0.5, 0.8, '*',\n", + " horizontalalignment='center',\n", + " verticalalignment='center',\n", + " transform=ax[idxc,idx].transAxes)\n", + " ax[idxc,idx].set_xlim(-1500,7000)\n", + " ax[idxc,idx].spines['right'].set_visible(False)\n", + " ax[idxc,idx].spines['left'].set_visible(False)\n", + " ax[idxc,idx].spines['top'].set_visible(False)\n", + " ax[idxc,0].set_ylabel(f\"{d_slide[s_slide]}\")\n", + " ax[0,2].legend(title='$\\mu$m')\n", + " plt.subplots_adjust(wspace=.001,hspace=.001)\n", + " plt.suptitle(f\"Distance to {s_target.split('_')[0]}\")\n", + " plt.tight_layout()\n", + " fig.savefig(f'./{s_date}/IntensityvsDistance_25s_{s_target}_by_slide.png',dpi=200)\n", + " #break" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python3.9.5", + "language": "python", + "name": "python3.9.5" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/GateCellTypes.ipynb b/GateCellTypes.ipynb new file mode 100755 index 0000000..bba7702 --- /dev/null +++ b/GateCellTypes.ipynb @@ -0,0 +1,573 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#load libraries\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import os\n", + "import copy\n", + "import seaborn as sns\n", + "import importlib\n", + "from matplotlib import cm\n", + "import matplotlib as mpl\n", + "mpl.rc('figure', max_open_warning = 0)\n", + "mpl.rcParams['pdf.fonttype'] = 42\n", + "mpl.rcParams['ps.fonttype'] = 42\n", + "mpl.rcParams['mathtext.fontset'] = 'custom'\n", + "mpl.rcParams['mathtext.it'] = 'Arial:italic'\n", + "mpl.rcParams['mathtext.rm'] = 'Arial'\n", + "codedir = os.getcwd()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#load cmif libraries\n", + "#os.chdir('/home/groups/graylab_share/OMERO.rdsStore/engje/Data/cmIF')\n", + "from mplex_image import visualize as viz, process, preprocess, gating" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os.chdir(codedir)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notes\n", + "\n", + "use CD45 to gate immune (CD3 more artifact)\n", + "\n", + "update 20200402: add SMT-Bx2-5 and HTA-33, simplified gating." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#set location of files\n", + "#load data\n", + "rootdir = f'{codedir}/paper_data'\n", + "# go to location of files\n", + "os.chdir(rootdir)\n", + "preprocess.cmif_mkdir(['GatingPlots'])\n", + "#os.listdir()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 3 define samples to work with/ image combos\n", + "ls_sample = ['20210402_SMT']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_data = pd.DataFrame()\n", + "for s_sample in ls_sample:\n", + " df_data = df_data.append(pd.read_csv(f'{s_sample}_ManualPositive.csv',index_col=0))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_data.columns" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "d_rename = {'CD4':'CD4_Ring','CD8':'CD8_Ring',\n", + " #'HER2':'HER2_Ring','ER':'ER_Nuclei'\n", + " }\n", + "df_data = df_data.rename(d_rename, axis=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Specify Gating Strategy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#parameters\n", + "\n", + "# cell types\n", + "ls_endothelial = ['CD31']\n", + "ls_immune = ['CD45','CD68'] \n", + "ls_tumor = ['CK7','CK19','Ecad'] \n", + "ls_prolif = ['Ki67']\n", + "\n", + "#tcell/myeloid\n", + "s_tcell = 'CD45' \n", + "s_bcell = 'CD20'\n", + "s_myeloid = 'CD68'\n", + "ls_immune_functional = ['PD1','CD44','prolif'] # not in dataset: 'FoxP3_Nuclei','GRNZB_Nuclei',\n", + "\n", + "#luminal/basal/mesenchymal\n", + "ls_luminal = ['CK19','CK7'] # not in dataset 'CK8_Ring'\n", + "ls_basal = ['CK5','CK14'] \n", + "ls_mes = ['CD44', 'Vim'] \n", + "ls_tumor_plus = ['Ecad'] + ['Lum']\n", + "ls_stromal_function = ['Vim','aSMA','PDPN']\n", + "ls_tumor_prolif = ['PCNA','Ki67','pHH3'] \n", + "\n", + "#index of cell line samples (i.e. 100% tumor)\n", + "ls_cellline_index = []\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#custom gating\n", + "df_data = gating.main_celltypes(df_data,ls_endothelial,ls_immune,ls_tumor,ls_cellline_index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_data.columns" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#add normal liver\n", + "df_data.loc[(~df_data.loc[:,ls_luminal].any(axis=1) & df_data.loc[:,'Ecad'] & df_data.loc[:,'tumor']),'celltype'] = 'epithelial'\n", + "df_data.loc[df_data.celltype == 'epithelial','tumor'] = False\n", + "df_data.loc[df_data.celltype == 'epithelial','epithelial'] = True\n", + "df_data.loc[df_data.celltype != 'epithelial','epithelial'] = False\n", + "df_data.epithelial = df_data.epithelial.astype('bool')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "importlib.reload(gating)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Perform Gating" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture\n", + "#simple gating\n", + "df_data = gating.proliferation(df_data,ls_prolif)\n", + "df_data = gating.immune_types(df_data,s_myeloid,s_bcell,s_tcell)\n", + "df_data = gating.cell_prolif(df_data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture\n", + "#cutom gating (skip)\n", + "'''\n", + "df_data = gating.immune_functional(df_data,ls_immune_functional)\n", + "df_data = gating.diff_hr_state(df_data,ls_luminal,ls_basal,ls_mes)\n", + "df_data = gating.celltype_gates(df_data,ls_tumor_prolif,s_new_name='TumorProlif',s_celltype='tumor')\n", + "#df_data = gating.celltype_gates(df_data,ls_tumor_plus,s_new_name='TumorDiffPlus',s_celltype='tumor')\n", + "df_data = gating.celltype_gates(df_data,ls_stromal_function,s_new_name='StromalType',s_celltype='stromal')\n", + "'''" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_data = gating.non_tumor(df_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Output Gating Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#check\n", + "ls_drop = ['ColI', 'ColIV', 'CD20', 'CD3', 'CD44', 'CK14',\n", + " 'CK5', 'ER', 'HER2', 'LamAC', 'PCNA', 'PD1', 'pHH3']\n", + "df_data.loc[:,df_data.dtypes=='object'].drop(ls_drop,axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#drop extra colums\n", + "df_gate = df_data.loc[:,df_data.dtypes!='bool'].drop(ls_drop,axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#handcrafted stromal populations (skip)\n", + "'''\n", + "d_rename_stroma = {'stromal_Vim_aSMA':'myofibroblast', 'stromal_aSMA':'myofibroblast', 'stromal___':'stromal', 'stromal_Vim':'fibroblast',\n", + " 'stromal_PDPN_Vim_aSMA':'myofibroblast', 'stromal_PDPN_Vim':'fibroblast', 'stromal_PDPN':'lymphatic',\n", + " 'stromal_PDPN_aSMA':'myofibroblast'}\n", + "df_gate.NonTumor = df_gate.NonTumor.replace(d_rename_stroma)\n", + "df_gate['FinalCell'] = df_gate.NonTumor.fillna(df_gate.CellProlif).fillna(df_gate.celltype)\n", + "df_gate.FinalCell = df_gate.FinalCell.replace({'tumor_nonprolif':'tumor','liver_nonprolif':'liver','liver_prolif':'liver'})\n", + "'''" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_gate.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "s_out = '20210402_SMT'\n", + "if not os.path.exists(f'{s_out}_GatedPositiveCellNames.csv'):\n", + " print('saving new csv')\n", + " df_gate.to_csv(f'{s_out}_GatedPositiveCellNames.csv')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#importlib.reload(viz)\n", + "s_out = '20210402_SMT'\n", + "f'{s_out}_GatedPositiveCellNames.csv'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_data = pd.read_csv(f'{s_out}_GatedPositiveCellNames.csv',index_col=0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#df_data['Stromal'] = df_data.StromalType.replace(d_rename_stroma)\n", + "#df_data['NonTumor'] = df_data.NonTumor.replace(d_rename_stroma)\n", + "#df_data['NonTumorFunc'] = df_data.NonTumorFunc.replace(d_rename_stroma)\n", + "#handcrafted stromal populations\n", + "#d_rename_stroma = {'stromal_Vim_aSMA':'myofibroblast', 'stromal_aSMA':'myofibroblast', 'stromal___':'stromal', 'stromal_Vim':'fibroblast',\n", + "# 'stromal_PDPN_Vim_aSMA':'myofibroblast', 'stromal_PDPN_Vim':'fibroblast', 'stromal_PDPN':'lymphatic',\n", + "# 'stromal_PDPN_aSMA':'myofibroblast'}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "(df_data.columns == 'FinalCell').any()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#combined cell type (run once)\n", + "if not (df_data.columns == 'FinalCell').any():\n", + " df_data.loc[df_data.celltype == 'tumor','FinalCell'] = df_data.loc[df_data.celltype == 'tumor','CellProlif']\n", + " df_data.loc[df_data.celltype != 'tumor','FinalCell'] = df_data.loc[df_data.celltype != 'tumor','celltype']\n", + " df_data.loc[df_data.celltype == 'immune','FinalCell'] = df_data.loc[df_data.celltype == 'immune','ImmuneType']\n", + "\n", + "#df_data.FinalCell.unique()\n", + "#df_data.to_csv(f'{s_out}_GatedPositiveCellNames.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ls_drop = df_data.loc[((df_data.index.str.contains('HTA')) & (df_data.FinalCell=='epithelial'))].index" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# get rid epithelial\n", + "# except HTAN\n", + "df_data['FinalCell'] = df_data.FinalCell.replace({'epithelial':'stromal'})\n", + "df_data = df_data.drop(ls_drop)\n", + "df_data['countme'] = True\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "s_grouper='slide_scene'\n", + "\n", + "#calculate proportions\n", + "for s_cell in df_data.columns[(df_data.dtypes=='object') & ~(df_data.columns.isin([s_grouper]))].tolist():\n", + " df_prop = viz.prop_positive(df_data,s_cell=s_cell,s_grouper=s_grouper)\n", + " # make annotations\n", + " df_annot=pd.DataFrame(data={'ID': df_prop.index.tolist()},index=df_prop.index)\n", + " lut = dict(zip(sorted(df_annot.ID.unique()),cm.tab10.colors))\n", + " g, df_plot_less = viz.prop_clustermap(df_prop,df_annot,i_thresh =.01,lut=lut)\n", + " g.savefig(f'./GatingPlots/{s_cell}_clustermap.png',dpi=150)\n", + " plt.close()\n", + " fig = viz.prop_barplot(df_plot_less,s_cell,colormap=\"Spectral\")\n", + " fig.savefig(f'./GatingPlots/{s_cell}_bar.png',dpi=200)\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#group by tissue\n", + "df_data['slide_scene'] = [item.split('_')[0] for item in df_data.slide_scene]\n", + "df_data_select = df_data.loc[~df_data.slide_scene.isin(['HTA-33_scene001','SMTBx1-16_scene001']),:]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#by tissue\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "s_grouper='slide_scene'\n", + "mpl.rcParams['pdf.fonttype'] = 42\n", + "mpl.rcParams['ps.fonttype'] = 42\n", + "\n", + "#calculate proportions\n", + "for s_cell in df_data.columns[(df_data.dtypes=='object') & ~(df_data.columns.isin([s_grouper]))].tolist():\n", + " df_prop = viz.prop_positive(df_data_select,s_cell=s_cell,s_grouper=s_grouper)\n", + " # make annotations\n", + " df_prop.to_csv(f'ManualGating_SMT_proportions_{s_cell}.csv')\n", + " df_annot=pd.DataFrame(data={'ID': df_prop.index.tolist()},index=df_prop.index)\n", + " lut = dict(zip(sorted(df_annot.ID.unique()),cm.tab10.colors))\n", + " g, df_plot_less = viz.prop_clustermap(df_prop,df_annot,i_thresh =.001,lut=lut)\n", + " g.savefig(f'./GatingPlots/{s_cell}_clustermap_tissue.pdf',dpi=150)\n", + " plt.close()\n", + " if df_plot_less.shape[1] < 8:\n", + " cmap = \"Spectral\"\n", + " elif df_plot_less.shape[1] < 11:\n", + " cmap = \"Paired\"\n", + " else:\n", + " cmap = \"tab20\"\n", + " fig = viz.prop_barplot(df_plot_less,s_cell,colormap=cmap)\n", + " fig.savefig(f'./GatingPlots/{s_cell}_bar_tissue.pdf',dpi=200)\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "s_date = '20210402'\n", + "d_crop = {'SMTBx2-5_scene001': (2000,9000),\n", + " 'SMTBx3_scene004': (20000,16000),\n", + " 'HTA-33_scene002': (3271, 607),\n", + " 'SMTBx1-16_scene003': (2440,220),\n", + " }\n", + "df_result = pd.DataFrame()\n", + "for s_tissue, tu_crop in d_crop.items():\n", + " df_scene = df_data.loc[df_data.index.str.contains(s_tissue)]\n", + " ls_index = df_scene.loc[((df_scene.DAPI_X > tu_crop[0]) & (df_scene.DAPI_X < tu_crop[0]+2500)) & (df_scene.DAPI_Y > tu_crop[1]) & (df_scene.DAPI_Y < tu_crop[1]+2500)].index\n", + " df_result = df_result.append(df_data.loc[ls_index])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#by tissue\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "s_grouper='slide_scene'\n", + "mpl.rcParams['pdf.fonttype'] = 42\n", + "mpl.rcParams['ps.fonttype'] = 42\n", + "d_rename = {'HTA-33':'Bx4', 'SMTBx1-16':'Bx1', 'SMTBx2-5':'Bx2', 'SMTBx3':'Bx3'}\n", + "\n", + "#calculate proportions\n", + "for s_cell in df_data.columns[(df_data.dtypes=='object') & ~(df_data.columns.isin([s_grouper]))].tolist():\n", + " df_prop = viz.prop_positive(df_result,s_cell=s_cell,s_grouper=s_grouper)\n", + " # make annotations\n", + " #df_prop.to_csv(f'ManualGating_SMT101_proportions_{s_cell}.csv')\n", + " df_annot=pd.DataFrame(data={'ID': df_prop.index.tolist()},index=df_prop.index)\n", + " lut = dict(zip(sorted(df_annot.ID.unique()),cm.tab10.colors))\n", + " g, df_plot_less = viz.prop_clustermap(df_prop,df_annot,i_thresh =.001,lut=lut)\n", + " g.savefig(f'./GatingPlots/{s_cell}_clustermap_tissue3.pdf',dpi=150)\n", + " plt.close()\n", + " if df_plot_less.shape[1] < 8:\n", + " cmap = \"Spectral\"\n", + " elif df_plot_less.shape[1] < 11:\n", + " cmap = \"Paired\"\n", + " else:\n", + " cmap = \"tab20\"\n", + " fig = viz.prop_barplot(df_plot_less.rename(d_rename),s_cell,colormap=cmap)\n", + " fig.set_size_inches(4.5, 2.3)\n", + " ax_list = fig.axes\n", + " ax_list[0].set_ylabel('')\n", + " ax_list[0].set_xlabel('Fraction of Cells')\n", + " ax_list[0].set_title('')\n", + " fig.suptitle('Gating Composition: Biopsies',x=0.5,y=0.9,fontsize=14)\n", + " plt.tight_layout()\n", + " fig.savefig(f'./GatingPlots/{s_cell}_bar_tissue3.png',dpi=200)\n", + " #fig.savefig(f'./{s_date}/{s_cell}_bar_tissue3.pdf',dpi=200)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "s_date" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python3.9.5", + "language": "python", + "name": "python3.9.5" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Normalize_Bx2-4.ipynb b/Normalize_Bx2-4.ipynb new file mode 100755 index 0000000..45a5f00 --- /dev/null +++ b/Normalize_Bx2-4.ipynb @@ -0,0 +1,1198 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#load libraries\n", + "\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "import numpy as np\n", + "import os\n", + "import copy\n", + "import seaborn as sns\n", + "import importlib\n", + "from scipy.signal import argrelmax, find_peaks, peak_widths\n", + "import scanpy as sc\n", + "from sklearn.cluster import KMeans\n", + "from sklearn.preprocessing import scale, minmax_scale\n", + "from sklearn.metrics import silhouette_score\n", + "import matplotlib as mpl\n", + "mpl.rc('figure', max_open_warning = 0)\n", + "mpl.rcParams['pdf.fonttype'] = 42\n", + "mpl.rcParams['ps.fonttype'] = 42\n", + "mpl.rcParams['mathtext.it'] = 'Arial:italic'\n", + "mpl.rcParams['mathtext.rm'] = 'Arial'\n", + "codedir = os.getcwd()\n", + "#load cmif libraries\n", + "#os.chdir('/home/groups/graylab_share/OMERO.rdsStore/engje/Data/cmIF')\n", + "from mplex_image import visualize as viz, process, preprocess, normalize" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os.chdir(codedir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "np.random.seed(222)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Table of contents \n", + "1. [Load Data](#load)\n", + "2. [Normalize](#norm)\n", + "3. [Visualize Normalization](#normviz)\n", + "4. [leiden for cell typing](#clusterlei)\n", + "5. [Leiden cluster](#clust1)\n", + "\n", + "\n", + "note:\n", + "\n", + " Could you make composite fraction bar graph only in following regions?\n", + "\n", + " Bx2: SMTBx2-5-Scene-001_ROI1-2000-9000-2500-2500\n", + " Bx3: SMTBx3-Scene-004_ROI2-20900-15494-2500-2500\n", + " Bx4: HTA-33-Scene-002_ROI1-3271-607-2500-2500\n", + "\n", + " If we can have it in Bx1\n", + " Bx: SMTBx1-Scene-003_ROI1-2440-220-2500-2500\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#load data\n", + "os.chdir(f'{codedir}/paper_data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "s_date = '20210402'\n", + "if not os.path.exists(s_date):\n", + " os.mkdir(s_date)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Load Data \n", + "\n", + "[contents](#contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os.chdir(f'{codedir}/paper_data')\n", + "df_file = pd.DataFrame(index=os.listdir())\n", + "df_file = df_file[df_file.index.str.contains('FilteredMeanIntensity_DAPI')]\n", + "df_file['tissue'] = [item.split('_')[1] for item in df_file.index]\n", + "df_file['dapi'] = ['DAPI' + item.split('y_DAPI')[1].split('.')[0] for item in df_file.index]\n", + "ls_sample = df_file.tissue.tolist()\n", + "d_dapi = dict(zip(df_file.tissue.tolist(),df_file.dapi.tolist()))\n", + "d_dapi.update({'JE-TMA-60': 'DAPI10_DAPI2'})\n", + "df_mi = pd.DataFrame()\n", + "df_xy = pd.DataFrame()\n", + "df_edge = pd.DataFrame()\n", + "\n", + "for s_sample in sorted(set(ls_sample)):\n", + " #if not s_sample.find('HTA')>-1:\n", + " print(f'loading {s_sample}')\n", + " df_mi = df_mi.append(pd.read_csv(f'{codedir}/paper_data/features_{s_sample}_FilteredMeanIntensity_{d_dapi[s_sample]}.csv', index_col=0))\n", + " df_xy = df_xy.append(pd.read_csv(f'{codedir}/paper_data/features_{s_sample}_CentroidXY.csv',index_col=0))\n", + " if os.path.exists(f'{codedir}/paper_data/features_{s_sample}_EdgeCells153pixels_CentroidXY.csv'):\n", + " df_edge = df_edge.append(pd.read_csv(f'{codedir}/paper_data/features_{s_sample}_EdgeCells153pixels_CentroidXY.csv',index_col=0))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#sorted(df_mi.columns[df_mi[~df_mi.index.str.contains('JE-TMA-60')].isna().sum() != 0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ls_marker = ['AR_nuclei', 'CD20_perinuc5', 'CD31_perinuc5', 'CD3_perinuc5', 'CD44_perinuc5', 'CD45_perinuc5',#'CD44_nucadj2',\n", + " 'CD4_perinuc5', 'CD68_perinuc5','CD8_perinuc5', 'CK14_cytoplasm', 'CK17_cytoplasm', 'CK19_cytoplasm', 'CK5_cytoplasm',\n", + " 'CK7_cytoplasm', 'CK8_cytoplasm', 'ColI_perinuc5', 'ColIV_perinuc5','CoxIV_perinuc5','EGFR_cytoplasm', 'ER_nuclei',\n", + " 'Ecad_cytoplasm', 'FoxP3_nuclei', 'GRNZB_nuclei', 'H3K27_nuclei','H3K4_nuclei', 'HER2_cellmem25','Ki67_nuclei',\n", + " 'LamAC_nuclei', 'PCNA_nuclei', 'PD1_perinuc5', 'PDPN_perinuc5','DAPI2_nuclei', # 'ER_nuclei25','HER2_cytoplasm','PgR_nuclei','Vim_nucadj2'\n", + " 'Vim_perinuc5', 'aSMA_perinuc5', 'pHH3_nuclei', 'pRB_nuclei', 'pS6RP_perinuc5','slide_scene',\n", + " ] # CD8R bad, 'gH2AX_nuclei' in R11 Bx3 not included\n", + "\n", + "df_mi = df_mi.loc[:,ls_marker]\n", + " \n", + "# old \n", + "#df_mi = df_mi.loc[:,['HER2_cellmem25', 'DAPI2_nuclei',# 'CD44_nucadj2', 'Vim_nucadj2','ER_nuclei25','HER2_cytoplasm',\n", + "# 'CD20_perinuc5', 'CD3_perinuc5', 'CD31_perinuc5', 'CD4_perinuc5','CD44_perinuc5', 'CD45_perinuc5', 'CD68_perinuc5', 'CD8_perinuc5',\n", + "# 'PD1_perinuc5', 'PDPN_perinuc5', 'Vim_perinuc5', 'aSMA_perinuc5','CK14_cytoplasm', 'CK17_cytoplasm', 'CK19_cytoplasm', 'CK5_cytoplasm',\n", + "# 'CK7_cytoplasm', 'Ecad_cytoplasm', 'ER_nuclei', 'Ki67_nuclei', 'LamAC_nuclei','PCNA_nuclei', 'pHH3_nuclei', 'slide_scene']]\n", + "\n", + "\n", + "df_mi['batch'] = [item.split('_')[0] for item in df_mi.index]\n", + "#df_mi['scene'] = [item.split('_')[1] for item in df_mi.index]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Deal with JE-TMA-60" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# markers in JE-TMA-60\n", + "#'JE-TMA-60_scene06', 'JE-TMA-60_scene08', 'JE-TMA-60_scene09', 'JE-TMA-60_scene10', 'JE-TMA-60_scene11', 'JE-TMA-60_scene13'\n", + "# R5 is CK17.PDPN.CD45.FoxP3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_R5 = pd.read_csv(f'{codedir}/paper_data/features_JE-TMA-60_FilteredMeanIntensity_DAPI5_DAPI2.csv',index_col=0)\n", + "df_R4 = pd.read_csv(f'{codedir}/paper_data/features_JE-TMA-60_FilteredMeanIntensity_DAPI4_DAPI2.csv',index_col=0)\n", + "df_R10 = df_mi[df_mi.batch=='JE-TMA-60']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ls_scene = set(df_R10.slide_scene)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ls_na = set([item.split('_cell')[0] for item in df_R5.index]) - set([item.split('_cell')[0] for item in df_R10.index])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#slect markers, scenes for normalization (based on JE-TMA-60 tissue loss)\n", + "ls_pos = ['HER2_cellmem25','CK19_cytoplasm','CK7_cytoplasm','CK8_cytoplasm','Ecad_cytoplasm','ER_nuclei','Ki67_nuclei','LamAC_nuclei',\n", + " 'PCNA_nuclei','pHH3_nuclei','Vim_perinuc5','DAPI2_nuclei','H3K27_nuclei','H3K4_nuclei', 'pRB_nuclei','pS6RP_perinuc5',\n", + " 'CoxIV_perinuc5','EGFR_cytoplasm']\n", + "ls_R5 = ['CK17_cytoplasm','PDPN_perinuc5','CD45_perinuc5','FoxP3_nuclei'] #\n", + "ls_R4 = ['pHH3_nuclei','CK14_cytoplasm','Ki67_nuclei','CK19_cytoplasm','CK5_cytoplasm','HER2_cellmem25',\n", + " 'Ecad_cytoplasm', 'ER_nuclei','CD44_perinuc5', 'PCNA_nuclei','aSMA_perinuc5','CD3_perinuc5','EGFR_cytoplasm']\n", + "ls_bad = ['CD20_perinuc5', 'CD31_perinuc5', 'CD4_perinuc5', 'CD68_perinuc5', 'CD8_perinuc5','PD1_perinuc5',\n", + " 'ColI_perinuc5', 'ColIV_perinuc5'] #'CK7_cytoplasm', #'LamAC_nuclei',\n", + "#ls_good = ['CK7_cytoplasm','Vim_perinuc5','LamAC_nuclei']\n", + "\n", + "#R4\n", + "df = df_mi[df_mi.batch!='JE-TMA-60']\n", + "df = df.append(df_R4.loc[:,ls_R4])\n", + "#R5\n", + "ls_index = df_R5.loc[df_R5.index.isin(df_R4.index)].index\n", + "df.loc[ls_index,ls_R5] = df_R5.loc[ls_index,ls_R5]\n", + "\n", + "#fill R6-8\n", + "ls_index = df_mi.loc[(df_mi.slide_scene.isin(ls_scene)) & (df_mi.index.isin(df_R4.index))].index\n", + "df.loc[ls_index,ls_pos] = df_R10.loc[ls_index,ls_pos]\n", + "\n", + "#\n", + "df['batch'] = [item.split('_')[0] for item in df.index]\n", + "#df['scene'] = [item.split('_')[1] for item in df.index]\n", + "df['slide_scene'] = [item.split('_cell')[0] for item in df.index]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## filter edge cells" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#filter out unwanted cells\n", + "d_filter = {#41 (not used)\n", + " 'JE-TMA-41_scene01':(df_xy.DAPI_Y > 5000),'JE-TMA-41_scene03':(df_xy.DAPI_Y > 5000),\n", + " 'JE-TMA-41_scene04':(df_xy.DAPI_Y < 1500),'JE-TMA-41_scene05':(df_xy.DAPI_Y > 5000),\n", + " 'JE-TMA-41_scene06':(df_xy.DAPI_Y < 1500),'JE-TMA-41_scene08':(df_xy.DAPI_Y < 1500),\n", + " 'JE-TMA-41_scene09':(df_xy.DAPI_Y > 5000),'JE-TMA-41_scene11':(df_xy.DAPI_Y < 1500),\n", + " #43\n", + " 'JE-TMA-43_scene09':(df_xy.DAPI_Y < 1200),'JE-TMA-43_scene14':(df_xy.DAPI_Y < 1200),\n", + " #60\n", + " 'JE-TMA-60_scene02':(df_xy.DAPI_X < 1500),'JE-TMA-60_scene05':(df_xy.DAPI_X < 1500),\n", + " 'JE-TMA-60_scene11':(df_xy.DAPI_Y < 1500),'JE-TMA-60_scene14':(df_xy.DAPI_X < 1500),\n", + " 'JE-TMA-60_scene06':(df_xy.DAPI_Y < 1500),'JE-TMA-60_scene08':(df_xy.DAPI_Y > 5000),\n", + " 'JE-TMA-60_scene10':(df_xy.DAPI_Y < 1500),\n", + " #63\n", + " 'JE-TMA-62_scene01':(df_xy.DAPI_Y > 5000),\n", + " 'JE-TMA-62_scene02':(df_xy.DAPI_X > 5000),'JE-TMA-62_scene03':(df_xy.DAPI_X < 1000),\n", + " 'JE-TMA-62_scene04':(df_xy.DAPI_Y < 1500),'JE-TMA-62_scene06':(df_xy.DAPI_X < 1000),\n", + " 'JE-TMA-62_scene08':(df_xy.DAPI_Y > 5000),'JE-TMA-62_scene10':(df_xy.DAPI_Y < 1500),\n", + " #'SMTBx1-16_scene001':(df_xy.DAPI_Y > 1), #keep scene 1 for manual thresholding\n", + " 'SMTBx2-3_scene002':(df_xy.DAPI_Y > 5000),'SMTBx3_scene004':(df_xy.DAPI_X <11000),\n", + " 'SMTBx3_scene005':(df_xy.DAPI_X > 0),'SMTBx4-3_scene001':(df_xy.DAPI_Y < 2400),\n", + " 'SMTBx2-5_scene002':(df_xy.DAPI_Y > 5000),'HTA-33_scene003':(df_xy.DAPI_Y > 9000)}\n", + "d_filter2 = {'JE-TMA-60_scene02':(df_xy.DAPI_Y > 4500)}\n", + "ls_filter_all = []\n", + "for s_scene, filtercon in d_filter.items():\n", + " ls_filter = df_xy[(df_xy.slide_scene==s_scene) & filtercon].index.tolist()\n", + " ls_filter_all = ls_filter_all + ls_filter\n", + "for s_scene, filtercon in d_filter2.items():\n", + " ls_filter = df_xy[(df_xy.slide_scene==s_scene) & filtercon].index.tolist()\n", + " ls_filter_all = ls_filter_all + ls_filter\n", + "#filter edge\n", + "ls_filter_all = ls_filter_all + df_edge.index.tolist()\n", + "df_filter_mi = df[(~df.index.isin(ls_filter_all))]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_cluster = df_filter_mi.loc[:,['HER2_cellmem25','slide_scene']]\n", + "df_cluster['cluster'] = 1\n", + "df_cluster.drop('HER2_cellmem25',axis=1,inplace=True)\n", + "import importlib\n", + "importlib.reload(viz)\n", + "%matplotlib inline\n", + "viz.plot_clusters(df_cluster,df_xy,s_num='few')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#match controls to biopsies\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "d_replace = {'BC44290-146': 'JE-TMA-41',\n", + " 'SMTBx2-3': 'JE-TMA-41',\n", + " 'SMTBx2-5':'JE-TMA-43',\n", + " 'SMTBx3':'JE-TMA-60',\n", + " 'SMTBx4-3':'JE-TMA-62'}\n", + "df_filter_mi.loc[:,'batch'] = df_filter_mi.batch.replace(d_replace)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#standardize the scenes\n", + "d_replace = { 'JE-TMA-41_scene13':'JE-TMA-41_scene14',\n", + " 'JE-TMA-41_scene12':'JE-TMA-41_scene13',\n", + " 'JE-TMA-62_scene13':'JE-TMA-62_scene14',\n", + " 'JE-TMA-62_scene12':'JE-TMA-62_scene13'}\n", + "df_filter_mi.loc[:,'scene'] = df_filter_mi.slide_scene.replace(d_replace)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "df_filter_mi.merge(df_xy.loc[:,['DAPI_X', 'DAPI_Y', 'nuclei_area', 'nuclei_eccentricity']],left_index=True,right_index=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_out = df_filter_mi.merge(df_xy.loc[:,['DAPI_X', 'DAPI_Y', 'nuclei_area', 'nuclei_eccentricity']],left_index=True,right_index=True)\n", + "len(df_out)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#2-23 contains NAs\n", + "#2-22 the NAs were filled with random gaussian data\n", + "# 0302 include scene 1 Bx1\n", + "# 0318 just Bx2 - 4, (Bx2-5)\n", + "# 20210324 has HTA9-1-33\n", + "if not os.path.exists('20210324_SMTBx1-4_JE-TMA-43_60_62_FilteredMeanIntensity.csv'):\n", + " print('saving csv')\n", + " #df_out.to_csv('20210223_SMTBx1-4_JE-TMA-41_60_62_BC44290-146.csv')\n", + " df_out.to_csv('20210324_SMTBx1-4_JE-TMA-43_60_62_FilteredMeanIntensity.csv') " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#2-23 contains NAs\n", + "#2-22 the NAs were filled with random gaussian data\n", + "# 0302 include scene 1 Bx1\n", + "# 0318 just Bx2 - 4, (Bx2-5)\n", + "if not os.path.exists('20210320_SMTBx2-4_JE-TMA-43_60_62_FilteredMeanIntensity.csv'):\n", + " print('saving csv')\n", + " #df_out.to_csv('20210223_SMTBx1-4_JE-TMA-41_60_62_BC44290-146.csv')\n", + " df_out.to_csv('20210320_SMTBx2-4_JE-TMA-43_60_62_FilteredMeanIntensity.csv') " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Normalization \n", + "\n", + "use ComBat.\n", + "\n", + "[contents](#contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_mi = pd.read_csv('20210320_SMTBx2-4_JE-TMA-43_60_62_FilteredMeanIntensity.csv',index_col=0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_mi.scene.unique()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ls_pos = ['HER2_cellmem25','CK19_cytoplasm','CK7_cytoplasm','CK8_cytoplasm','Ecad_cytoplasm','ER_nuclei','Ki67_nuclei','LamAC_nuclei',\n", + " 'PCNA_nuclei','pHH3_nuclei','Vim_perinuc5','DAPI2_nuclei','H3K27_nuclei','H3K4_nuclei', 'pRB_nuclei','pS6RP_perinuc5',\n", + " 'CoxIV_perinuc5','EGFR_cytoplasm']\n", + "ls_R5 = ['CK17_cytoplasm','PDPN_perinuc5','CD45_perinuc5','FoxP3_nuclei'] #\n", + "ls_R4 = ['pHH3_nuclei','CK14_cytoplasm','Ki67_nuclei','CK19_cytoplasm','CK5_cytoplasm','HER2_cellmem25',\n", + " 'Ecad_cytoplasm', 'ER_nuclei','CD44_perinuc5', 'PCNA_nuclei','aSMA_perinuc5','CD3_perinuc5','EGFR_cytoplasm']\n", + "ls_bad = ['CD20_perinuc5', 'CD31_perinuc5', 'CD4_perinuc5', 'CD68_perinuc5', 'CD8_perinuc5','PD1_perinuc5',\n", + " 'ColI_perinuc5', 'ColIV_perinuc5']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#select normalization scenes\n", + "ls_R10_scene = ['scene06', 'scene08', 'scene09', 'scene10', 'scene11', 'scene13']\n", + "ls_R10 = ['HER2_cellmem25', 'CK19_cytoplasm', 'CK7_cytoplasm', 'Ecad_cytoplasm', 'ER_nuclei', 'Ki67_nuclei', 'LamAC_nuclei',\n", + " 'PCNA_nuclei','pHH3_nuclei', 'Vim_perinuc5','CD44_perinuc5','DAPI2_nuclei', #adding following:\n", + " 'CK8_cytoplasm','CoxIV_perinuc5', 'EGFR_cytoplasm', 'H3K27_nuclei', 'H3K4_nuclei', 'pRB_nuclei', 'pS6RP_perinuc5']\n", + "#note: CK17 may have quenching artifact; PDPN not good in Bx1, so just CD45 important\n", + "#'CK17_cytoplasm','PDPN_perinuc5', #'FoxP3_nuclei' not in full set\n", + "ls_R5 = ['PDPN_perinuc5','CD45_perinuc5','FoxP3_nuclei', 'aSMA_perinuc5','CD3_perinuc5'] # aSMA because N breast, scene 01 better than 07 for immune\n", + "ls_R5_scene = ['scene01','scene03','scene04']\n", + "#old ls_R4 = ['pHH3_nuclei','CK14_cytoplasm','Ki67_nuclei','CK19_cytoplasm','CK5_cytoplasm','HER2_cellmem25',\n", + "# 'Ecad_cytoplasm', 'ER_nuclei','CD44_perinuc5', 'PCNA_nuclei','aSMA_perinuc5','CD3_perinuc5','DAPI2_nuclei']\n", + "#can scene 7 be good control for CD3 and CK14 and CK5?, yes. R1 doen't add much\n", + "ls_R4 = [ 'CK14_cytoplasm', 'CK5_cytoplasm','CK17_cytoplasm'] #'CD3_perinuc5',\n", + "ls_R4_scene = ['scene02','scene07']\n", + "ls_bad = ['CD20_perinuc5', 'CD31_perinuc5', 'CD4_perinuc5', 'CD68_perinuc5', 'CD8_perinuc5','PD1_perinuc5']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "set(df_mi.batch)\n", + "#df_mi = df_mi.loc[df_mi.batch!='JE-TMA-60']\n", + "df_mi['slide_scene'] = df_mi.scene\n", + "df_mi['scene'] = [item.split('_')[1] for item in df_mi.slide_scene]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#dropped 60\n", + "df_norm_all=pd.DataFrame(index=df_mi.dropna().index)\n", + "\n", + "#not dropped 60\n", + "df_norm_all=pd.DataFrame(index=df_mi.index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#1 fit on scenes that are good through round 10 and markers that are positive on those scenes \"pos\"\n", + "for s_type in ['R4','R5','R10']:\n", + " if s_type == 'R10':\n", + " ls_pos = ls_R10\n", + " ls_scene = ls_R10_scene\n", + "\n", + " #2 fit on scenes that are good until R4, and R1-4 markers\n", + " if s_type == 'R4':\n", + " ls_pos = ls_R4\n", + " ls_scene = ls_R4_scene # + ls_R5_scene + ls_R10_scene \n", + "\n", + " #3 fit on scene that are good until R5, and R5 markers\n", + " if s_type == 'R5':\n", + " ls_pos = ls_R5\n", + " ls_scene = ls_R5_scene\n", + "\n", + " #fit\n", + " b_control = ((df_mi.index.str.contains('JE-TMA')) & (df_mi.scene.isin(ls_scene)) & (df_mi.loc[:,ls_pos].isna().sum(axis=1)==0))\n", + " data = df_mi.loc[b_control,ls_pos].T\n", + " batch = df_mi.loc[b_control,'batch']\n", + " gamma_star, delta_star, stand_mean, var_pooled = normalize.combat_fit(data, batch)\n", + " #transform\n", + " #data = df_mi.loc[df_mi.batch!='SMTBx1-16',df_mi.dtypes=='float64'].drop(['DAPI_X','DAPI_Y'],axis=1).T\n", + " data = df_mi.loc[df_mi.batch!='SMTBx1-16',ls_pos].T\n", + " batch = df_mi.loc[df_mi.batch!='SMTBx1-16','batch']\n", + " bayesdata = normalize.combat_transform(data,batch,gamma_star,delta_star,stand_mean, var_pooled)\n", + " df_norm = bayesdata.T\n", + " df_norm_all = df_norm_all.merge(df_norm,left_index=True,right_index=True,how='left')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_norm_all.tail()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# run after #1, 2 and 3\n", + "df_norm_all = df_norm_all.merge(df_mi.loc[:,['batch','DAPI_X','DAPI_Y','scene','nuclei_area','nuclei_eccentricity']],left_index=True,right_index=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#old check\n", + "df_norm = df_norm.merge(df_mi.loc[:,['batch','DAPI_X','DAPI_Y','scene','nuclei_area','nuclei_eccentricity']],left_index=True,right_index=True)\n", + "#df_mi.loc[b_control,:].drop(['DAPI_X','DAPI_Y'],axis=1).groupby('batch').mean()\n", + "#df_mi[df_mi.index.str.contains('JE-TMA')].drop(['DAPI_X','DAPI_Y'],axis=1).groupby('batch').std()\n", + "#check\n", + "df_norm.loc[b_control,:].drop(['DAPI_X','DAPI_Y'],axis=1).groupby('batch').mean()\n", + "#df_norm[df_norm.index.str.contains('JE-TMA')].drop(['DAPI_X','DAPI_Y'],axis=1).groupby('batch').std()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#df_norm_all.to_csv('20210320_SMTBx2-4_JE-TMA-43_60_62_normalized.csv')\n", + "#df_norm_all.to_csv('20210325_SMTBx2-4_JE-TMA-43_60_62_normalized.csv')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Umap Visualize Normalization \n", + "\n", + "[contents](#contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#s_sample = '20210320_SMTBx2-4_JE-TMA-43_60_62'\n", + "s_sample = '20210325_SMTBx2-4_JE-TMA-43_60_62'\n", + "df_norm_all = pd.read_csv(f'{s_sample}_normalized.csv',index_col=0)\n", + "df_norm_all.rename({'nuclei_area':'area','nuclei_eccentricity':'eccentricity','DAPI_X':'DAPIX',\n", + " 'DAPI_Y':\"DAPIY\"},axis=1, inplace=True)\n", + "df_norm_all.columns = [item.split('_')[0] for item in df_norm_all.columns]\n", + "df_norm_all['slide'] = [item.split('_')[0] for item in df_norm_all.index]\n", + "df_norm_all['scene'] = [item.split('_')[1] for item in df_norm_all.index]\n", + "df_norm_all['slide_scene'] = [item.split('_cell')[0] for item in df_norm_all.index]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_norm_all = df_norm_all.loc[~df_norm_all.slide_scene.isin(['JE-TMA-43_scene01','JE-TMA-62_scene01'])]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# visualize\n", + "%matplotlib inline\n", + "s_type = 'w-60_no01'\n", + "#adata = sc.AnnData(df_norm_all.loc[:,df_norm_all.dtypes=='float64'].drop(['DAPIX','DAPIY'],axis=1)) \n", + "ls_drop = ['DAPIX','DAPIY','DAPI2','LamAC','pHH3','FoxP3','CoxIV',\n", + " 'H3K27','H3K4','pRB','pS6RP','aSMA','PDPN'] #aSMA, PDPN not well norm\n", + "adata = sc.AnnData(df_norm_all.dropna().loc[:,df_norm_all.dtypes=='float64'].drop(ls_drop,axis=1))\n", + "adata.obs['batch'] = df_norm_all.dropna().loc[:,'batch']\n", + "adata.obs['scene'] = df_norm_all.dropna().loc[:,'scene'].replace({'scene001':'Bx', 'scene002':'Bx','scene003':'Bx', 'scene004':'Bx', 'scene005':'Bx'})\n", + "adata.obs['tissue'] = df_norm_all.dropna().loc[:,'slide']\n", + "# reduce dimensionality (PCA)\n", + "adata.raw = adata\n", + "#reduce dimensionality\n", + "sc.tl.pca(adata, svd_solver='auto')\n", + "#sc.pl.pca(adata)\n", + "sc.pl.pca_variance_ratio(adata, log=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# calculate neighbors\n", + "n_neighbors = 31\n", + "n_pcs=len(adata.var.index) - 1\n", + "results_file = f'{s_sample}_{n_neighbors}neighbors_{n_pcs}pcs_{len(adata.var.index)}markers.h5ad'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "results_file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "d_celline = {'scene02':'HCC1143',\n", + " 'scene03':'HCC3153',\n", + " 'scene04':'N.Breast',\n", + " 'scene05':'T47D',\n", + " 'scene06':'T47D',\n", + " 'scene07':'Tonsil',\n", + " 'scene08':'BT474',\n", + " 'scene09':'BT474',\n", + " 'scene10':'AU565',\n", + " 'scene11':'AU565',\n", + " 'scene12':'MDAMB436',\n", + " 'scene13':'MDAMB436',\n", + " 'scene14':'MDAMB436'}\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "\n", + "# calculate neighbors\n", + "if os.path.exists(results_file):\n", + " adata = sc.read_h5ad(results_file)\n", + " print('loading umap')\n", + "else:\n", + " # calculate neighbors \n", + " print('calculating umap')\n", + " sc.pp.neighbors(adata, n_neighbors=n_neighbors, n_pcs=n_pcs)\n", + " sc.tl.umap(adata)\n", + " #save results\n", + " if not os.path.exists(results_file):\n", + " adata.write(results_file)\n", + "\n", + "# umap plus scenes\n", + "fig,ax = plt.subplots(figsize=(3,2.5),dpi=600)\n", + "figname = f'UmapScene_{s_type}_{n_pcs+1}markers.png'\n", + "sc.pl.umap(adata, color='scene',save=figname,title=f'TMA Core',ax=ax)\n", + "\n", + "\n", + "# umap plus tissue\n", + "fig,ax = plt.subplots(figsize=(3,2.5),dpi=600)\n", + "figname = f'UmapTissue_{s_type}_{n_pcs+1}markers.png'\n", + "adata.obs['Tissue'] = adata.obs['tissue'].replace({'SMTBx2-5':'Bx2', 'SMTBx3':'Bx3','SMTBx4-3':'Bx4'})\n", + "sc.pl.umap(adata, color='Tissue',save=figname,title=f'Tissue',ax=ax)\n", + "\n", + "\n", + "# umap plus cell line\n", + "adata.obs['Subtype'] = adata.obs.scene.replace(d_celline)\n", + "fig,ax = plt.subplots(figsize=(3,2.5),dpi=600)\n", + "figname = f'UmapSubtype_{s_type}_{n_pcs+1}markers.png'\n", + "sc.pl.umap(adata, color='Subtype',save=figname,title=f'Subtype',ax=ax)\n", + "\n", + "\n", + "#umap plot\n", + "ls_marker = adata.var.index.tolist()\n", + "figname = f\"Umap_{s_type}_{n_pcs+1}markers.png\"\n", + "axes = sc.pl.umap(adata, color=ls_marker,wspace=.25,save=figname,vmin='p1.5',vmax='p98.5',ncols=3,show=False)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "#umap plot\n", + "ls_marker = adata.var.index.tolist()\n", + "figname = f\"Umap_{s_type}_{n_pcs+1}markers.png\"\n", + "fig = sc.pl.umap(adata, color=ls_marker,wspace=.25,vmin='p1.5',vmax='p98.5',ncols=3,show=False,return_fig=True)\n", + "ax_list = fig.axes\n", + "for ax in ax_list:\n", + " ax.set_title(ax.get_title(),fontsize=28)\n", + "fig.savefig(f'figures/{figname}',dpi=600)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## cluster leiden \n", + "\n", + "[contents](#contents)\n", + "\n", + "cluster on the markers that are normalized well" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "resolution = 0.45\n", + "results_file = f'{s_sample}_{n_neighbors}neighbors_{n_pcs}pcs_{len(adata.var.index)}markers_leiden{resolution}.h5ad'\n", + "#save\n", + "if not os.path.exists(results_file):\n", + " sc.tl.leiden(adata,resolution=resolution)\n", + "else:\n", + " adata = sc.read_h5ad(results_file)\n", + " print('loading leiden') \n", + "fig,ax = plt.subplots(figsize=(3,2.5),dpi=600)\n", + "figname=f'leiden_{resolution}.png'\n", + "sc.pl.umap(adata, color='leiden',ax=ax,save=figname)\n", + "#seaborn clustermap\n", + "df_p = pd.DataFrame(data=adata.raw.X,index=adata.obs.index,columns=adata.var.index)\n", + "df_p['leiden'] = adata.obs['leiden']\n", + "g = sns.clustermap(df_p.groupby('leiden').mean(),z_score=1,figsize=(4,4),cmap='viridis',\n", + " vmin=-1.5,vmax=1.5) \n", + "#g.savefig(f'./figures/clustermap_leiden.png',dpi=200)\n", + "marker_genes = df_p.groupby('leiden').mean().iloc[:,g.dendrogram_col.reordered_ind].columns.tolist()\n", + "categories_order = df_p.groupby('leiden').mean().iloc[g.dendrogram_row.reordered_ind,:].index.tolist()\n", + "#scanpy matrixplot\n", + "fig,ax = plt.subplots(figsize=(5,5), dpi=200)\n", + "figname=f'Matrixplot_leiden_{resolution}.png'\n", + "sc.pl.matrixplot(adata, var_names=marker_genes, groupby=f'leiden',title='',categories_order=categories_order,\n", + " ax=ax,save=figname,standard_scale='var',colorbar_title='Relative\\nintensity',\n", + " #var_group_positions=[(3,23),(24,31),(32,42),(43,51)],\n", + " #var_group_labels=['tumor','T-cell','muscle\\n +AF','immune\\n+stroma'],\n", + " #var_group_rotation=0\n", + " )\n", + "\n", + "#save\n", + "if not os.path.exists(results_file):\n", + " adata.write(results_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Leiden barplots \n", + "\n", + "\n", + "[contents](#contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ls_order = [\n", + " 'Bx2','Bx3','Bx4',#'JE-TMA-43_scene01','JE-TMA-62_scene01',\n", + " 'JE-TMA-43_scene02', 'JE-TMA-62_scene02',\n", + " 'JE-TMA-43_scene03', 'JE-TMA-62_scene03', 'JE-TMA-43_scene04',\n", + " 'JE-TMA-62_scene04', 'JE-TMA-43_scene05', 'JE-TMA-62_scene05',\n", + " 'JE-TMA-43_scene06','JE-TMA-60_scene06', 'JE-TMA-62_scene06', 'JE-TMA-43_scene07',\n", + " 'JE-TMA-62_scene07', 'JE-TMA-43_scene08','JE-TMA-60_scene08', 'JE-TMA-62_scene08',\n", + " 'JE-TMA-43_scene09','JE-TMA-60_scene09', 'JE-TMA-62_scene09','JE-TMA-43_scene10', 'JE-TMA-62_scene10','JE-TMA-60_scene10',\n", + " 'JE-TMA-43_scene11', 'JE-TMA-60_scene11', 'JE-TMA-62_scene11', 'JE-TMA-43_scene13',\n", + " 'JE-TMA-62_scene12', 'JE-TMA-43_scene14','JE-TMA-60_scene13', 'JE-TMA-62_scene13'] " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ls_order_r = ls_order[::-1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#load original\n", + "'''\n", + "s_sample = '20210320_SMTBx2-4_JE-TMA-43_60_62'\n", + "n_neighbors = 30\n", + "n_pcs = 19\n", + "n_markers = n_pcs+1\n", + "resolution = 0.5\n", + "results_file = f'{s_sample}_{n_neighbors}neighbors_{n_pcs}pcs_{n_markers}markers_leiden{resolution}.h5ad'\n", + "adata1 = sc.read_h5ad(results_file) \n", + "\n", + "d_cluster = {'14': '14: Basal',\n", + "'5': '5: T cell',\n", + "'12': '12: T cell',\n", + "'10': '10: Myoepithelial',\n", + "'1': '1: Mesenchymal',\n", + "'16': '16: Prolif.',\n", + "'15': '15: Vim+ FB (Bx3)',\n", + "'11': '11: Vim+ FB (Bx4)',\n", + "'13': '13: Vim+ FB (Bx2)',\n", + "'7': '7: HER2++',\n", + "'9': '9: EGFR+ Basal',\n", + "'3': '3: HER2+',\n", + "'8': '8: HER2++, Ecad-',\n", + "'0': '0: ER+ (Bx4)',\n", + "'2': '2: ER+, PCNA+ ',\n", + "'4': '4: ER+, EGFR+ (Bx3)',\n", + "'6': '6: ER+ (Bx2)'}\n", + "d_clust_names = dict(zip([item[0] for item in d_cluster.items()],[item[1].split(': ')[1] for item in d_cluster.items()]))\n", + "'''" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#load\n", + "s_sample = '20210325_SMTBx2-4_JE-TMA-43_60_62'\n", + "n_neighbors = 31\n", + "n_pcs = 17\n", + "n_markers = n_pcs+1\n", + "resolution = 0.45\n", + "results_file = f'{s_sample}_{n_neighbors}neighbors_{n_pcs}pcs_{n_markers}markers_leiden{resolution}.h5ad'\n", + "adata1 = sc.read_h5ad(results_file) \n", + "print(results_file)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if resolution == 0.5:\n", + " d_cluster = {'14': '14: Basal','12': '12: T cell','16': '16: Prolif.','7': '7: ER+ (Bx2)','13': '13: Luminal (N.Breast)',\n", + " '1': '1: ER+ PCNA+ (T47D)','0': '0: ER+ (Bx4)','15': '15: ER+ CK8++ (Bx4)','4': '4: ER+, EGFR+ (Bx3)','18': '18: ER+, EGFR+ (Bx3)',\n", + " '17': '17: (Bx3)','10': '10: FB (Bx4)','11': '11: FB (Bx2)','3': '3: CD44+','9': '9: CD44+', '8': '8: EGFR+ Basal',\n", + " '5': '5: HER2++','6': '6: HER2+','2': '2: HER2++, Ecad-',}\n", + "elif resolution == 0.45:\n", + " d_cluster = {'15':'15: Basal',\n", + " '12':'12: T cell',\n", + " '16': '16: prolif.',\n", + " '5':'5: ER+, EGFR+ (Bx3)',\n", + " '0':'0: ER+ (Bx4)',\n", + " '1':'1: ER+, PCNA+',\n", + " '7':'7: ER- (Bx2)',\n", + " '9':'9: ER+ (Bx2)',\n", + " '8':'8: EGFR+ Basal',\n", + " '4':'4: HER2+',\n", + " '3':'3: HER2+',\n", + " '6':'6: HER2+, Ecad-',\n", + " '2':'2: Mesenchymal',\n", + " '10':'10: Mesenchymal',\n", + " '14':'14: fibroblast',\n", + " '11':'11: fibroblast',\n", + " '13':'13: fibroblast'}\n", + "d_clust_names = dict(zip([item[0] for item in d_cluster.items()],[item[1].split(': ')[1] for item in d_cluster.items()]))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "mpl.rcParams['pdf.fonttype'] = 42\n", + "mpl.rcParams['ps.fonttype'] = 42\n", + "#sns.set(font_scale=1.19)\n", + "#seaborn clustermap\n", + "df_p = pd.DataFrame(data=adata1.raw.X,index=adata1.obs.index,columns=adata1.var.index)\n", + "df_p['leiden'] = adata1.obs['leiden']\n", + "g = sns.clustermap(df_p.groupby('leiden').mean().rename({'eccentricity':'eccen.'},axis=1).rename(d_cluster, axis=0),\n", + " z_score=1,figsize=(6.2,6),cmap='viridis',\n", + " vmin=-2,vmax=2,cbar_pos=(.05, .89, .10, .05),cbar_kws={'orientation': 'horizontal','label':'Z-score'}) #(left, bottom, width, height),\n", + "g.savefig(f'./{s_date}/clustermap_leiden_{resolution}_{n_markers}.pdf',dpi=300)\n", + "g.savefig(f'./{s_date}/clustermap_leiden_{resolution}_{n_markers}.png',dpi=300)\n", + "marker_genes = df_p.groupby('leiden').mean().iloc[:,g.dendrogram_col.reordered_ind].columns.tolist()\n", + "categories_order = df_p.groupby('leiden').mean().iloc[g.dendrogram_row.reordered_ind,:].index.tolist()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# stacked bar vertical\n", + "\n", + "df = pd.DataFrame(data=adata1.raw.X,index=adata1.obs.index,columns=adata1.var.index)\n", + "df[f'leiden'] = [int(item) for item in adata1.obs.leiden]\n", + "s_markers = n_markers\n", + "k=resolution\n", + "\n", + "df['slide'] = [item.split('_')[0] for item in df.index]\n", + "df['slide_scene'] = [item.split('_cell')[0] for item in df.index]\n", + "df['slide_scene'] = df.slide_scene.replace({'SMTBx2-5_scene001':'Bx2', 'SMTBx2-5_scene002':'Bx2',\n", + " 'SMTBx3_scene004':'Bx3', 'SMTBx4-3_scene001':'Bx4',\n", + " 'SMTBx4-3_scene002':'Bx4'})#.replace(d_order)\n", + "df['scene'] = [item.split('_')[1] for item in df.index]\n", + "df_prop = (df.groupby([f'leiden','slide_scene']).PCNA.count())/(df.groupby(['slide_scene']).PCNA.count())\n", + "df_prop = df_prop.unstack().fillna(value=0).T\n", + "\n", + "fig,ax=plt.subplots(figsize=(5,6), dpi=200)\n", + "df_prop['slide'] =[item.split('_')[0] for item in df_prop.index]\n", + "#df_prop['scene'] =[item.split('_')[1] for item in df_prop.index]\n", + "df_prop = df_prop.loc[ls_order_r]\n", + "df_prop.columns = [str(item) for item in df_prop.columns]\n", + "#df_prop.rename(d_order).rename(d_cluster,axis=1).plot(kind='barh',stacked=True,ax=ax,legend=True,cmap='tab20',width=0.9)\n", + "df_prop.plot(kind='barh',stacked=True,ax=ax,legend=True,cmap='tab20',width=0.9)\n", + "ax.legend(bbox_to_anchor=(1.05, 1.00),ncol=1, fancybox=True,title='Cluster ID')\n", + "ax.set_xlabel('Fraction of Cells')\n", + "ax.set_ylabel('Tissue')\n", + "ax.set_title('')\n", + "plt.tight_layout()\n", + "fig.savefig(f'./{s_date}/StackedBar_{s_markers}markers_{k}Clusters_vertical.pdf')\n", + "fig.savefig(f'./{s_date}/StackedBar_{s_markers}markers_{k}Clusters_vertical.png')\n", + "#plt.close()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#save the cluster ID, not hte annotation\n", + "#df_prop.to_csv(f'{s_sample}_{n_markers}markers_leiden{resolution}_frac_pos.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "import matplotlib.ticker as tic\n", + "#SMT\n", + "fig,ax=plt.subplots(figsize=(2.8,3.2),dpi=200)\n", + "df_plot = df_prop.loc[['Bx2','Bx3','Bx4'],df_prop.dtypes=='float64'].T[::-1]\n", + "df_plot.plot(kind='barh',ax=ax,legend=True,width=.9)\n", + "ax.legend(title='Bx', loc='upper left',fancybox=True,borderpad=.2,bbox_to_anchor=(1.05, 1.05))\n", + "ax.set_xlabel('Fraction of Cells')\n", + "ax.set_ylabel('')\n", + "fig.suptitle(f'Cluster Composition: Biopsies',x=.5, y=.92)\n", + "for tick in ax.yaxis.get_major_ticks():\n", + " tick.tick1line.set_markersize(0)\n", + " tick.tick2line.set_markersize(0)\n", + "temp = tic.LinearLocator(numticks=18)\n", + "ax.yaxis.set_minor_locator(temp)\n", + "plt.grid(b=True, which='minor', axis='y')\n", + "plt.tight_layout()\n", + "fig.savefig(f'./{s_date}/Barplot_SMT{s_markers}_K{k}.pdf')\n", + "fig.savefig(f'./{s_date}/Barplot_SMT{s_markers}_K{k}.png')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ls_order = ['Bx2', 'Bx3', 'Bx4','AU565-2','AU565-3', 'AU565-4', 'BT474-2','BT474-3', 'BT474-4', \n", + " 'HCC1143-2', 'HCC1143-4', 'HCC3153-2', 'HCC3153-4', #'JE-TMA-43_scene01','JE-TMA-62_scene01', 'JE-TMA-43_scene10',\n", + " 'MDAMB-436-2','MDAMB-436-3', 'MDAMB-436-4', 'T47D-2','T47D-3', 'T47D-4',\n", + " 'N.Breast-2', 'N.Breast-4', 'tonsil-2', 'tonsil-4']\n", + "d_order = {#'\n", + " 'JE-TMA-43_scene02':'HCC1143-2', 'JE-TMA-62_scene02':'HCC1143-4',\n", + " 'JE-TMA-43_scene03':'HCC3153-2', 'JE-TMA-62_scene03':'HCC3153-4', 'JE-TMA-43_scene04':'N.Breast-2',\n", + " 'JE-TMA-62_scene04':'N.Breast-4', 'JE-TMA-43_scene05':'T47D-2', 'JE-TMA-62_scene05':'T47D-4',\n", + " 'JE-TMA-43_scene06':'T47D-2', 'JE-TMA-62_scene06':'T47D-4', 'JE-TMA-43_scene07':'tonsil-2',\n", + " 'JE-TMA-62_scene07':'tonsil-4', 'JE-TMA-43_scene08':'BT474-2', 'JE-TMA-62_scene08':'BT474-4',\n", + " 'JE-TMA-43_scene09':'BT474-2', 'JE-TMA-62_scene09':'BT474-4', 'JE-TMA-43_scene10':'AU565-2','JE-TMA-62_scene10':'AU565-4',\n", + " 'JE-TMA-43_scene11':'AU565-2', 'JE-TMA-62_scene11':'AU565-4', 'JE-TMA-43_scene13':'MDAMB-436-2',\n", + " 'JE-TMA-62_scene12':'MDAMB-436-4', 'JE-TMA-43_scene14':'MDAMB-436-2', 'JE-TMA-62_scene13':'MDAMB-436-4',\n", + " 'JE-TMA-60_scene13':'MDAMB-436-3', 'JE-TMA-60_scene11':'AU565-3', 'JE-TMA-60_scene10':'AU565-3',\n", + " 'JE-TMA-60_scene09':'BT474-3', 'JE-TMA-60_scene08':'BT474-3', 'JE-TMA-60_scene06':'T47D-3'}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "#stacked bar vertical tissue\n", + "df['coreID'] = df.slide_scene.replace(d_order)\n", + "df['celltype'] = df.leiden.astype('str').replace(d_clust_names)\n", + "df_prop = (df.groupby([f'celltype','coreID']).PCNA.count())/(df.groupby(['coreID']).PCNA.count())\n", + "df_prop = df_prop.unstack().fillna(value=0).T\n", + "\n", + "fig,ax=plt.subplots(figsize=(5,3.7), dpi=200)\n", + "df_prop['slide'] =[item.split('_')[0] for item in df_prop.index]\n", + "ls_order_r = ls_order[::-1]\n", + "df_prop = df_prop.loc[ls_order_r]\n", + "df_prop.columns = [str(item) for item in df_prop.columns]\n", + "df_prop.plot(kind='barh',stacked=True,ax=ax,legend=True,cmap='tab20',width=0.9) #.rename(d_order).rename(d_clust_names,axis=1)\n", + "ax.legend(loc='upper left', bbox_to_anchor=(1.1,1.02),ncol=1, fancybox=True,title='Cluster Annotation')\n", + "ax.set_xlabel('Fraction of Cells')\n", + "ax.set_ylabel('Tissue')\n", + "ax.set_title('Cluster Composition: Biopsies Plus Controls')\n", + "plt.tight_layout()\n", + "fig.savefig(f'./{s_date}/StackedBar_{s_markers}markers_{k}Clusters_withcontrols_vert.pdf')\n", + "fig.savefig(f'./{s_date}/StackedBar_{s_markers}markers_{k}Clusters_withcontrols_vert.png')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#stacked bar horizontal\n", + "df['coreID'] = df.slide_scene.replace(d_order)\n", + "df['celltype'] = df.leiden.astype('str').replace(d_clust_names)\n", + "df_prop = (df.groupby([f'celltype','coreID']).PCNA.count())/(df.groupby(['coreID']).PCNA.count())\n", + "df_prop = df_prop.unstack().fillna(value=0).T\n", + "\n", + "fig,ax=plt.subplots(figsize=(10,2.5), dpi=200)\n", + "df_prop['slide'] =[item.split('_')[0] for item in df_prop.index]\n", + "#df_prop['scene'] =[item.split('_')[1] for item in df_prop.index]\n", + "df_prop = df_prop.loc[ls_order]\n", + "df_prop.columns = [str(item) for item in df_prop.columns]\n", + "df_prop.plot(kind='bar',stacked=True,ax=ax,legend=True,cmap='tab20',width=0.9) #.rename(d_order).rename(d_clust_names,axis=1)\n", + "ax.legend(loc='upper center', bbox_to_anchor=(1.5, 1.05),ncol=2, fancybox=True,title='Cluster Annotation')\n", + "ax.set_ylabel('Fraction of Cells')\n", + "ax.set_xlabel('Tissue')\n", + "ax.set_title('')\n", + "plt.tight_layout()\n", + "fig.savefig(f'./{s_date}/StackedBar_{s_markers}markers_{k}Clusters_withcontrols.pdf')\n", + "fig.savefig(f'./{s_date}/StackedBar_{s_markers}markers_{k}Clusters_withcontrols.png')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#plot all groups spatially \n", + "from matplotlib.colors import ListedColormap, LinearSegmentedColormap\n", + "newcmap = ListedColormap(mpl.cm.tab20.colors)#ListedColormap(mpl.cm.tab20b.colors + mpl.cm.tab20c.colors)\n", + "from mplex_image import analyze\n", + "df_pos = analyze.celltype_to_bool(df_p,'leiden')\n", + "df_xy = df_mi.loc[df_pos.index]\n", + "ls_scene = ['SMTBx2-5_scene001', 'SMTBx3_scene004', 'SMTBx4-3_scene001', 'SMTBx4-3_scene002']\n", + "#ls_scene = ['JE-TMA-62_scene04', 'JE-TMA-43_scene04','JE-TMA-62_scene07','JE-TMA-43_scene07']\n", + "for s_slide in ls_scene:\n", + " fig,ax = plt.subplots(figsize=(10,10),dpi=200) #10,10\n", + " #plot negative cells\n", + " df_scene = df_xy[df_xy.index.str.contains(s_slide)]\n", + " ax.scatter(data=df_scene,x='DAPI_X',y='DAPI_Y',color='silver',s=0.1,label=f'')\n", + " for idxs, s_color_int in enumerate(range(len(df_pos.columns))):\n", + " s_color = str(s_color_int)\n", + " if len(df_xy[(df_xy.slide_scene==s_slide) & (df_pos.loc[:,s_color])])>=1:\n", + " #plot positive cells\n", + " ax.scatter(data=df_xy[(df_xy.slide_scene==s_slide) & (df_pos.loc[:,s_color])],x='DAPI_X',y='DAPI_Y',\n", + " label=f'{s_color}',s=0.1,color=newcmap.colors[idxs])\n", + " #break\n", + " ax.set_title(f\"{s_slide}\", fontsize=16)\n", + " ax.axis('equal')\n", + " ax.set_ylim(ax.get_ylim()[::-1])\n", + " #ax.set_xticklabels('')\n", + " #ax.set_yticklabels('')\n", + " #break\n", + " plt.legend(markerscale=10) \n", + " fig.savefig(f'{codedir}/paper_data/GatingPlots/{s_slide}_clustering_scatterplot.png')\n", + " #break" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if not os.path.exists(f'{s_sample}_{n_markers}markers_leiden{resolution}.csv'):\n", + " print('saving csv')\n", + " df.to_csv(f'{s_sample}_{n_markers}markers_leiden{resolution}.csv')\n", + " df_prop.to_csv(f'{s_sample}_{n_markers}markers_leiden{resolution}_frac_pos.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "f'{s_sample}_{n_markers}markers_leiden{resolution}.csv'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "f'{s_sample}_{n_markers}markers_leiden{resolution}_frac_pos.csv'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python3.9.5", + "language": "python", + "name": "python3.9.5" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/mplex_image/20210312_visualize.py b/mplex_image/20210312_visualize.py new file mode 100755 index 0000000..f9f86b9 --- /dev/null +++ b/mplex_image/20210312_visualize.py @@ -0,0 +1,288 @@ +#### +# title: analyze.py +# +# language: Python3.6 +# date: 2019-05-00 +# license: GPL>=v3 +# author: Jenny +# +# description: +# python3 library to visualize cyclic data and analysis +#### + +#load libraries +import matplotlib as mpl +import matplotlib.pyplot as plt +import pandas as pd +import numpy as np +import os +import skimage +from skimage import io, segmentation +import tifffile +import copy +import napari +import seaborn as sns +from sklearn.cluster import KMeans +from sklearn.preprocessing import scale + +#napari +def load_crops(viewer,s_crop,s_tissue): + ls_color = ['blue','green','yellow','red','cyan','magenta','gray','green','yellow','red','cyan','magenta', + 'gray','gray','gray','gray','gray','gray','gray','gray'] + print(s_crop) + #viewer = napari.Viewer() + for s_file in os.listdir(): + if s_file.find(s_tissue)>-1: + if s_file.find(s_crop) > -1: + if s_file.find('ome.tif') > -1: + with tifffile.TiffFile(s_file) as tif: + array = tif.asarray() + omexml_string = tif.ome_metadata + for idx in range(array.shape[0]): + img = array[idx] + i_begin = omexml_string.find(f'Channel ID="Channel:0:{idx}" Name="') + i_end = omexml_string[i_begin:].find('" SamplesPerPixel') + s_marker = omexml_string[i_begin + 31:i_begin + i_end] + viewer.add_image(img,name=s_marker,rgb=False,visible=False,blending='additive',colormap=ls_color[idx],contrast_limits = (np.quantile(img,0),(np.quantile(img,0.9999)+1)*1.5)) + elif s_file.find('SegmentationBasins') > -1: + label_image = io.imread(s_file) + viewer.add_labels(label_image, name='cell_seg',blending='additive',visible=False) + cell_boundaries = segmentation.find_boundaries(label_image,mode='outer') + viewer.add_labels(cell_boundaries,blending='additive') + else: + label_image = np.array([]) + print('') + return(label_image) + +def pos_label(viewer,df_pos,label_image,s_cell): + ''' + df_pos = boolean dataframe, s_cell = marker name + ''' + #s_cell = df_pos.columns[df_pos.columns.str.contains(f'{s_cell}_')][0] + #get rid of extra cells (filtered by DAPI, etc) + li_index = [int(item.split('_')[-1].split('cell')[1]) for item in df_pos.index] + label_image_cell = copy.deepcopy(label_image) + label_image_cell[~np.isin(label_image_cell, li_index)] = 0 + li_index_cell = [int(item.split('_')[-1].split('cell')[1]) for item in df_pos[df_pos.loc[:,s_cell]==True].index] + label_image_cell[~np.isin(label_image_cell,li_index_cell )] = 0 + viewer.add_labels(label_image_cell, name=f'{s_cell.split("_")[0]}_seg',blending='additive',visible=False) + return(label_image_cell) + +#jupyter notbook +#load manual thresholds +def new_thresh_csv(df_mi,d_combos): + #make thresh csv's + df_man = pd.DataFrame(index= ['global']+ sorted(set(df_mi.slide_scene))) + for s_type, es_marker in d_combos.items(): + for s_marker in sorted(es_marker): + df_man[s_marker] = '' + return(df_man) + +def load_thresh_csv(s_sample): + #load + df_man = pd.read_csv(f'thresh_JE_{s_sample}.csv',header=0,index_col = 0) + #reformat the thresholds data and covert to 16 bit + ls_index = df_man.index.tolist() + ls_index.remove('global') + df_thresh = pd.DataFrame(index = ls_index) + ls_marker = df_man.columns.tolist() + for s_marker in ls_marker: + df_thresh[f'{s_marker}_global'] = df_man[df_man.index=='global'].loc['global',f'{s_marker}']*256 + df_thresh[f'{s_marker}_local'] = df_man[df_man.index!='global'].loc[:,f'{s_marker}']*256 + + df_thresh.replace(to_replace=0, value = 12, inplace=True) + return(df_thresh) + +def threshold_postive(df_thresh,df_mi): + ''' + #make positive dataframe to check threhsolds #start with local, and if its not there, inesrt the global threshold + #note, this will break if there are two biomarker locations # + ''' + ls_scene = sorted(df_thresh.index.tolist()) + ls_sub = df_mi.columns[df_mi.dtypes=='float64'].tolist() + ls_other = [] + df_pos= pd.DataFrame() + d_thresh_record= {} + for s_scene in ls_scene: + ls_index = df_mi[df_mi.slide_scene==s_scene].index + df_scene = pd.DataFrame(index=ls_index) + for s_marker_loc in ls_sub: + s_marker = s_marker_loc.split('_')[0] + # only threshold markers in .csv + if len(set([item.split('_')[0] for item in df_thresh.columns]).intersection({s_marker})) != 0: + #first check if local threshold exists + if df_thresh[df_thresh.index==s_scene].isna().loc[s_scene,f'{s_marker}_local']==False: + #local + i_thresh = df_thresh.loc[s_scene,f'{s_marker}_local'] + df_scene.loc[ls_index,s_marker_loc] = df_mi.loc[ls_index,s_marker_loc] >= i_thresh + #otherwise use global + elif df_thresh[df_thresh.index==s_scene].isna().loc[s_scene,f'{s_marker}_global']==False: + i_thresh = df_thresh.loc[s_scene,f'{s_marker}_global'] + df_scene.loc[ls_index,s_marker_loc] = df_mi.loc[ls_index,s_marker_loc] >= i_thresh + else: + ls_other = ls_other + [s_marker] + i_thresh = np.NaN + d_thresh_record.update({f'{s_scene}_{s_marker}':i_thresh}) + else: + ls_other = ls_other + [s_marker] + df_pos = df_pos.append(df_scene) + print(f'Did not threshold {set(ls_other)}') + return(d_thresh_record,df_pos) + +def plot_positive(s_type,d_combos,df_pos,d_thresh_record,df_xy,b_save=True): + ls_color = sorted(d_combos[s_type]) + ls_bool = [len(set([item.split('_')[0]]).intersection(set(ls_color)))==1 for item in df_pos.columns] + ls_color = df_pos.columns[ls_bool].tolist() + ls_scene = sorted(set(df_xy.slide_scene)) + ls_fig = [] + for s_scene in ls_scene: + #negative cells = all cells even before dapi filtering + df_neg = df_xy[(df_xy.slide_scene==s_scene)] + #plot + fig, ax = plt.subplots(2, ((len(ls_color))+1)//2, figsize=(18,12)) #figsize=(18,12) + ax = ax.ravel() + for ax_num, s_color in enumerate(ls_color): + s_marker = s_color.split('_')[0] + s_min = d_thresh_record[f"{s_scene}_{s_marker}"] + #positive cells = positive cells based on threshold + ls_pos_index = (df_pos[df_pos.loc[:,s_color]]).index + df_color_pos = df_neg[df_neg.index.isin(ls_pos_index)] + if len(df_color_pos)>=1: + #plot negative cells + ax[ax_num].scatter(data=df_neg,x='DAPI_X',y='DAPI_Y',color='silver',s=1) + #plot positive cells + ax[ax_num].scatter(data=df_color_pos, x='DAPI_X',y='DAPI_Y',color='DarkBlue',s=.5) + + ax[ax_num].axis('equal') + ax[ax_num].set_ylim(ax[ax_num].get_ylim()[::-1]) + ax[ax_num].set_title(f'{s_marker} min={int(s_min)} ({len(df_color_pos)} cells)') + else: + ax[ax_num].set_title(f'{s_marker} min={(s_min)} ({(0)} cells') + fig.suptitle(s_scene) + ls_fig.append(fig) + if b_save: + fig.savefig(f'./SpatialPlots/{s_scene}_{s_type}_manual.png') + return(ls_fig) + +#gating analysis +def prop_positive(df_data,s_cell,s_grouper): + #df_data['countme'] = True + df_cell = df_data.loc[:,[s_cell,s_grouper,'countme']].dropna() + df_prop = (df_cell.groupby([s_cell,s_grouper]).countme.count()/df_cell.groupby([s_grouper]).countme.count()).unstack().T + return(df_prop) + +def prop_clustermap(df_prop,df_annot,i_thresh,lut,figsize=(10,5)): + for s_index in df_prop.index: + s_subtype = df_annot.loc[s_index,'ID'] # + df_prop.loc[s_index, 'ID'] = s_subtype + species = df_prop.pop("ID") + row_colors = species.map(lut) + + #clustermap plot wihtout the low values -drop less than i_threh % of total + df_plot = df_prop.fillna(0) + if i_thresh > 0: + df_plot_less = df_plot.loc[:,df_plot.sum()/len(df_plot) > i_thresh] + i_len = len(df_prop) + i_width = len(df_plot_less.columns) + g = sns.clustermap(df_plot_less,figsize=figsize,cmap='viridis',row_colors=row_colors) + return(g,df_plot_less) + +def prop_barplot(df_plot_less,s_cell,colormap="Spectral",figsize=(10,5),b_sort=True): + i_len = len(df_plot_less) + i_width = len(df_plot_less.columns) + fig,ax = plt.subplots(figsize=figsize) + if b_sort: + df_plot_less = df_plot_less.sort_index(ascending=False) + df_plot_less.plot(kind='barh',stacked=True,width=.9, ax=ax,colormap=colormap) + ax.set_title(s_cell) + ax.set_xlabel('Fraction Positive') + ax.legend(bbox_to_anchor=(1.01, 1)) + plt.tight_layout() + return(fig) + +def plot_color_leg(lut,figsize = (2.3,3)): + #colors + series = pd.Series(lut) + df_color = pd.DataFrame(index=range(len(series)),columns=['subtype','color']) + + series.sort_values() + df_color['subtype'] = series.index + df_color['value'] = 1 + df_color['color'] = series.values + + fig,ax = plt.subplots(figsize = figsize,dpi=100) + df_color.plot(kind='barh',x='subtype',y='value',width=1,legend=False,color=df_color.color,ax=ax) + ax.set_xticks([]) + ax.set_ylabel('') + ax.set_title(f'subtype') + plt.tight_layout() + return(fig) + +#cluster analysis + +def cluster_kmeans(df_mi,ls_columns,k,b_sil=False): + ''' + log2 transform, zscore and kmens cluster + ''' + df_cluster_norm = df_mi.loc[:,ls_columns] + df_cluster_norm_one = df_cluster_norm + 1 + df_cluster = np.log2(df_cluster_norm_one) + + #select figure size + i_len = k + i_width = len(df_cluster.columns) + + #scale date + df_scale = scale(df_cluster) + + #kmeans cluster + kmeans = KMeans(n_clusters=k, random_state=0).fit(df_scale) + df_cluster.columns = [item.split('_')[0] for item in df_cluster.columns] + df_cluster[f'K{k}'] = list(kmeans.labels_) + g = sns.clustermap(df_cluster.groupby(f'K{k}').mean(),cmap="RdYlGn_r",z_score=1,figsize=(3+i_width/3,3+i_len/3)) + if b_sil: + score = silhouette_score(X = df_scale, labels=list(kmeans.labels_)) + else: + score = np.nan + return(g,df_cluster,score) + +def plot_clusters(df_cluster,df_xy,s_num='many'): + s_type = df_cluster.columns[df_cluster.dtypes=='int64'][0] + print(s_type) + ls_scene = sorted(set(df_cluster.slide_scene)) + ls_color = sorted(set(df_cluster.loc[:,s_type].dropna())) + d_fig = {} + for s_scene in ls_scene: + #negative cells = all cells even before dapi filtering + df_neg = df_xy[(df_xy.slide_scene==s_scene)] + #plot + if s_num == 'many': + fig, ax = plt.subplots(3, ((len(ls_color))+2)//3, figsize=(18,12),dpi=200) + else: + fig, ax = plt.subplots(2, 1, figsize=(7,4),dpi=200) + ax = ax.ravel() + for ax_num, s_color in enumerate(ls_color): + s_marker = s_color + #positive cells = poitive cells based on threshold + ls_pos_index = (df_cluster[df_cluster.loc[:,s_type]==s_color]).index + df_color_pos = df_neg[df_neg.index.isin(ls_pos_index)] + if len(df_color_pos)>=1: + #plot negative cells + ax[ax_num].scatter(data=df_neg,x='DAPI_X',y='DAPI_Y',color='silver',s=1) + #plot positive cells + ax[ax_num].scatter(data=df_color_pos, x='DAPI_X',y='DAPI_Y',color='DarkBlue',s=.5) + + ax[ax_num].axis('equal') + ax[ax_num].set_ylim(ax[ax_num].get_ylim()[::-1]) + if s_num == 'many': + ax[ax_num].set_xticklabels('') + ax[ax_num].set_yticklabels('') + ax[ax_num].set_title(f'{s_color} ({len(df_color_pos)} cells)') + else: + ax[ax_num].set_xticklabels('') + ax[ax_num].set_yticklabels('') + ax[ax_num].set_title(f'{s_color} ({(0)} cells') + + fig.suptitle(s_scene) + d_fig.update({s_scene:fig}) + return(d_fig) diff --git a/mplex_image/__init__.py b/mplex_image/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/mplex_image/__pycache__/__init__.cpython-37.pyc b/mplex_image/__pycache__/__init__.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..e9e21ea43539c8fec8022c4e2ca2ed3f24ba18e9 GIT binary patch literal 168 zcmZ?b<>g`k0+B@*<3RLd5CH>>K!yVl7qb9~6oz01O-8?!3`HPe1o10SKO;XkRX@Eb zzqFtjNF`S0BqqfdXCxM->ihe;2Knn1r4$F3dFffH`YwqjiTcU8o^JZN1v#k| m@tL`a>8bkh@tJv+~Dl0Sq literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/__init__.cpython-38.pyc b/mplex_image/__pycache__/__init__.cpython-38.pyc new file mode 100755 index 0000000000000000000000000000000000000000..95b1ebcf89b3ac8e5d6771994e5da82497fe35b3 GIT binary patch literal 172 zcmWIL<>g`k0+B@*<3RLd5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;x_uenx(7s(yM= zerZ85kV>q~Nlc0_&PXgu)%W*x4f5A3N+}L5$p>;%^U||Y^<5H667`dFJ>B$k3vyB` m;xls-(^K{1<1_OzOXB183My}L*yQG?l;)(`fvoxr#0&thsw+nT literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/__init__.cpython-39.pyc b/mplex_image/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8859bad976b8d35261d71b09d350981ffb6867d GIT binary patch literal 193 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/analyze.cpython-38.pyc b/mplex_image/__pycache__/analyze.cpython-38.pyc new file mode 100755 index 0000000000000000000000000000000000000000..ff95f608c701a86fd4065f8474368007dfac2d2d GIT binary patch literal 8173 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%0AUicKG}Ol;}MsjX%L0VF^OZk>|6q@!0Gi;Qqmj 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 V96x*ZjI-pNsVzCR(g&rx{{x5HNNE57 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/cmif.cpython-37.pyc b/mplex_image/__pycache__/cmif.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..5e4ca2bcbd8ac42b871f6bc469d77bf2c315b820 GIT binary patch literal 25835 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%0Aa1Vg0+t1-xw1e6H%OCj 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/codex.cpython-38.pyc b/mplex_image/__pycache__/codex.cpython-38.pyc new file mode 100755 index 0000000000000000000000000000000000000000..0010b932a71880adc7f1144c0f7f1ed0f9e89de1 GIT binary patch literal 22289 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 C_JW-N literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/features.cpython-38.pyc b/mplex_image/__pycache__/features.cpython-38.pyc new file mode 100755 index 0000000000000000000000000000000000000000..c869dfe56a5f5d655b3d7d95984ef0e0f9a7b81a GIT binary patch literal 21150 zcmc(Hd5~PkdEdM@_w4NK?0sMX3=RTV5Lg@}NQ$C)0i;Bcgg}s%sFA4gVEXOu%+Ag% z`n@HA^&6TFs0eb*n6hQXmIPCioJuNH zsoHYF{C;1*xpo&2C8a7@%s1Wr`t{vk|GuN=CkF>J5&UzafBI9`UX4Wln33LpaYP=+ z$9>(3L{vms^+?ftTSZIab}=g7v0_}l6UBslCyPn>P8C!5w(F^tbTMu5-6+DD;(&x> z_2f#nm|Ynx4zA>iIZ2BnZKyc3k}u{ZZq*FgE#R&XR!|#i{U}>6Mw{E`-z0L}^w%r3O^?O{=(DJ*{$T2%$adeJZbp5!$QXuSV1;LIpLZ z#@~z-_o)driSPaD88xGJ;mHB@0kuc%Md+X^sC{^Lk9t-eRQDj|ka|uXR!0yztmf2F zJUgPESI5;nQs&eNbrLB@)hTrv-}kEf)cyEArp~MPsE3i`xcZ>_3H2yK^Xh_nOg)a! z3H5|}61h&Qv+5kaPburg$lMFJIWy$R%GxOhS*Id7suiSFqHJRLk86jYv-r4sK)guJ z@*}?G+sb+?+KPJiM%0hqv|2GghW^>>A1GuB__tVn)oB#!P}#^0U=i))aR2&R-YAs2vdW0p!<=r5u7D^1=is%_6Z@zG1sk!2uSk%q6 zRTp37t0<-9E@6mHeDL|FE`D%cEB8gOiKx?9tT`uG1(WXf0NY`!N+$@we=c*{wJ}iB>YqpYmgV+_#pk$0MyY-c0!E>4={s zU9b^K`Dv7u@}pCcR>sfxsRbLenOJ{JCc?S0$|>}goI+!*;?&E!J+qgt7RrT%>J>-f z$;u^17n%!&RmAJAycxusYgnk5$=FI$IT{=&@)BY>?O@Wm5>*u|^;SZ2l?6i!NGM%u zt~FFyW6pzErHNsTyOrwdRn4&qCR7zWr%`cASUwGRp{Z9&j`HRP^aLNL+K+=wPceh; zl3r^pm%?R{xm2z!bDkSmH|-bPz^VpTEy&`*YmIi+*h<-5E)M8wV=+vU#bU@V)`&Ug z=$NNRkm7PRMPfNCC4ULaj@a+yVks+YO<5^hpTg6=DJT2xOnC#5cQNJByG(g^sxhIM z>Xb}1s7!U#PhqODa4^+rKP}UIMkcG$1ef5J47cqMnp{nWp!br6TaFobeT=EcN#;pTf)q2lx^jn6&b1q7p1&8_-6@dm<1C|W>HF}kZM_Jj&sQiS=!;Xe%CWv_!bk;+>6_#70ZC(qVd{lb$i@*mlWq;|*XgGQOn} zlM&g7S*c?Hvk}AA%VL(&q%un}KP%~+r9poXIid?u%vEyz@!l=TZP^}~A8Xofa|X@a z<#w7oeS*(VliUkZ9O!JD(n5C{dy_8}6Wlwcg}{7Y0vvEwE@9I;cba1t%TK9#`20E4 z<8lRc=a@Sx+)vT=ch<@y6+6q)4hcf--Ft5 zgb9-x=y@YZHeYpgy}TNvgvn@Ubt{L-%YH1W>cWDf9k9L%%5qa65pWg$>4y=na0=W1 z$WA+}{UJmaBQ5m(T;!FhmhDA0aDd~`j$#~a9^Y2va>|Qt#C+?fRg24E(>~Ip>mQJg ztdy@*SJqZ~4sslxHK#&|Lar|dtXnuzZLF?=g@8*l6+p#l;81qwx}(9<0EM}837tgC zf<$!%C`1K`^6IJsmTWgx1s+wFgD7g9ix(4h7pLoDF$Urm;}Qv?Oo^^kw~beF(eX-1 zucL4`3lfRNFi06N;1Nr6E!&Y+P&`Ct+ls>n@iIZmuoqWw%Mol;Zq4Z0FX9#Hrndw@1H`7VYRteCUaNXbh3Bf@edAZ%LKR0MpxvuZ;rVJq z2KB6Nx^CeqqO{ekg%!tJYAPwIx&U0%EYzE?9V=jX>R06yMik%eI;suV!}xLN3zf2q zL)uxC;?0=P;kYHtD|Oy5Yci&}go11*xm2%e;*(%RJ1ba~C3mHam5qJ~`g#(N^wWHv zLH%KA!nn6C*t97U<-LaNW~Ih+Rt^)OKfnUw)kee7x5-ZWCV=3FEs*_s9RYA-*{2-z zWn0hsk)a5nW~3Ewj3CWM+I%<7vud28xQeQnfS?3;aI%%cX_vr6St0ll*h9$EIS~PV zyx2w@(;eGLa6jC%1qMaIkrN99w(<41WIk|$VF#e`#|mu@Sm*$e!h&wD6v*`mX1gGx z^QM5IJHN>{3g&!d0W3&fHGBp*x+m#bjE)?@FUhexmlO`84>1|*I-(DgJO&aBKys*7 z%JmXhSU1vW5;XDL4PxHaRVT;^ZUdV!_^LP%f+jYj%lWq)+{F|h^Bof^p^ICW!vM1Y z)oB30pGAElNJJ8bnJ2I_6IQIl)_Htmm%e~}cV^|}qGaXMkv?X|U1+U8aJXmC%yuzi zh_?g~zJMhyn-QCZfVMNpbG8|+#9E_zdCdW@=GX|sF6LaP4o>H&QdvVTD5lCKlWXU$ z8vOKGc9pA{TpD=)MT9n!AH4X!d9S(!68LyOiVa$nAR6aj1q3gF$`18)LZ)Dq_+*DMGI@kbzbZ%IocEwv>L zdqHKwv}E|MU=)`0w)IK|SSVIYVV5O|R9Xo?p|aSAzo`aW$;Q7!EQeU&$2Zby2(8Rd zM-UojdyzZY_$qRYAjhBWkOO&<<5!SlblHB}(uE!Jj7c63dB!ErxgGLMNS+JGGbwqN zcF1GOIfgt_lIQ1l$YaV$c^Nf*4QC{foay<@0z|LNpB0gJS9u?%OP1VAOI5e9u+}hV z;%il`wAHc(Lk`czg=QU`6QV?6x#?67y9Jz#i~+$xTINA2#XNPTOo8&@!bM2BVC~vb z$1k2LT{wIGiTShV9-6;!?$rF*XHUJS^nOR5e)9OUr_MZh{6z@x&he8cPnRmEO5qv{ z+vF6mCwV5LapkUvZB8U@ZSX6X6@@e}rIdUwpN>b?hK+ibo`|&;iM%-PkSG`+Mm&~6GVmNSDwr&C$og)XvrklKt z?f>)6JMZY15z<8xzGMi6O8pAR-@vESsvs^i@Z;9Xp}u>qf3v?npG;r-InICoVjL&}kVtDJih#k#HYA_vA4$dllrjRB zveuaSv$Hl83Op@mNNrvAkQQ%ph zhg$f01k}C}qcniiCtZaq2v1`-c>;a0HQ*0y09hG)gq+YafX10$C6;6Qx}V-iB2P*s zkpgW4%keWC>E1Mdpo)_V(#8W@N=y5(o-z|*nGgJsWu|dL#a{XHr>vcO1wNj+j=tWo z$;~@`J8JrNoo6a~f^%m?S~t)eZ)M-ME{}Kv8(F_QT3J7f(&CV$22u7P%T_t*$?=Ew=p8JvTguXzisQ!%FOho zT9L-XeYrV9Cigdba)+tNoo%ObC3)l5BCT;K^hbeq#%mMO+i|rILfJUtlWPAmVIsmC zQ)(YZA0ebqU$?MQ4`7DTpYEDnCZd_}Cu(`DB>R=4tx1`yNxV1Z&1~%Qr*2w)sy5_L zs)NJ;{?x|oO^g#(O;o>$9J_mR+`}A5+tZVFNZy)|5)KP~n#Oz|@u%CX(w|myz#~!p zd;T=q!J3XTAKG}YY3=7DKzq~v*v8(Qm|GySHz4KAw08M3>KNXP=?{5>K3Yoi1x*)N zH*r}vv#doW{TYsLH>^(Gi1oxy_r>n(i{0NBJ98tpnDA#yxyA4?{8{xd z{@$w|>Arg-irw}TZ^c{rx1*Q8h0jqx@pfFl;paE@_3(#Bg{M9y9N_UADdC7oaLGK@ zQ}Xh6fG3Tj{fYH23F4|ZA;wcG>9zk9Am^{uJ%BJiG3pfqC=KRfYGtVmp%kVT^FRt~ z8(3)a!E2$($H=Hoxw2&Pnli-v0|OCa&=d%i>$M3LDE3OBo7Rb7{45k+t4-G_z1U-> z(n~mEHnV_$qBo<8YjaoOc!8MYztWZk8DpHLZ9E9Fe=M!gG8)$n^+%Z=GvMD*w~r?Q-hs&z1dg{ zQf+N<5Tjz$z`nQ`XV9EbQ#_Xr5@!5iDng@!bmgkoT&vKnaWkfEMvSzg40+LYF8h~d#|Rye5kdg7xW zKY9E;uLO}>QD`R{USPwP8+F$%-4Ww&qS(%`1!)m2Xr8?-3rQyDeaI5zx=OmH=dJ}K z-MFW#<;G&&(M@FB91E8g_e6V974!lg>5r5AB*_;@Xb;hYBts;5ko5t~ys;D?KiL~O zRg6RP;gpMmrCzpxRb+P34ZI9P*cw_#&|r65*!7Ce0uSzrs#FcQLZnDrKy+L7o|^EE82wFR0p-Sxd^u_m@LxsG+Wv}qk* zkIh%ySLgQkNDr&11ftp!WDI?oZE~3qt2L_){V3xiPbAh>Vd-}C8dEqR`X@+cL4sr` zG;5DZB8@!E^ieD$F4HwwUrWXW4)f@086r%=fim551^t^@{;Parz{RdAy;`oXIj&YL zjjLJ9dW!K-&|hZ!7W2j_A-h3*#nFq9W4vaGJ=?86&4&qWV+u_PG=bkaNIggjB^ozK zVIM(lSYE;ILklHXj9)5UsxHF3y3pnv44P#dq*#K;cS(g}aA2TtBT3;~l~=1ljMP}7 z9%LhEt>=O+#<;^#ZVBQU7F#9Am>{bT;u6;Dd?UrGg;o7DqI!~~#Rrh$@NN(<%hw>X z8QF|cvyx;@aXAIkcVyMSi^uL~Ktz*iPe6xBxvcU{9K#2?fCUEv4-&0M7rAWu^d#qpy7KhZX>(Z#@E_>1$U2M7%Ws z0Wh;obQ|EY3OJYjAp}4Oi~eA3uq`0=jDFTn_lSZyJtiwYWZzK;ZTX9;9y7i%l8R}!+yRa`sF*K-*6{wQ0@Lg4C3R?g5Ilb zA+0O^-9^y*Vo&b733{XO;G&>68VY)&YA=MaQN$-zK?FU7A?WSJj39(`Bj`c!+d|B2qoqnh-I3qAixmyicIiNeGX5e?sjS;cW`J4){~;ToAerity(9 zQ(-;`fg-#eGA$6{ZOR{^*oa;A9f)qztr>AeKq(N;CgY{-IgYgb z{(cIQ{w|aSLABck3aSunemh)6_ljt=i*-XZ+Ep6%haej5QpfNIL9_o&bpoQ%5Z-`mKI(r;SvgRql}}{vQiIutrl<0z537K66jUQHjcGvD=AE zBzOiD)5T2MsDB2GQ@=q%0Y?m2`p+?RjiiH`^_z@-g5=MWe3Ilk$)`wuj^r-V;gu1f~yv$ z(8OK(OC$o_iOBU|Bk2d=a0mGfrV^3>deLih5kdU>{$tzP%R_t=pC4J;dl!JY*4vAh zG;IKamo;_rbNjXcT>TY3@4#FAcNp!UTm5$#?T5C2TuWt0Uqra3>mEabZhw=Z-y-=c z$?GH{s>opeJw^q*axh`Zf$Bj2115@TJ9T-j48s}Zj=#^e-v*gGAgEP;jS2sdp(2`JhxM-liOYjV zt}A|RJ*0S6!0t=mkG|up<63ab5>LpvxZ!8N%F*~d2{{`iA8}5di;B;pxQB`-+uV>@ zf1hQ}zXVc*_jc$&CmIv@+qdJ?n9zL^UaIENfbn8Ak+eK8c}%*5b8Rh3sDQCcrD7J1 zslrn>te}%aUZ6(^A4();#4fjPmgEduDJKy(^eUGhlOnM!#1RTWBlt_S|Nbly*UVQT zH*WtsfWQDgvJ^fRgNp}z3u;&n)XLZ3KcK8n*nSca#lB$+uz~f;gFovvi%?p`6?j0v zLPc>V>QnC(qkT$Wf?p53F#A&hM@LM$*b(3p$WmbW${;l*@a&LShSCZEUu;}&KxoOp zHZ&k9Nn_)>{DznH2H{7M1~5$-o6I!}I_k_d>v4c3hz3zVd(Bcwcs=}2$(y-LUYmJg zIm?PDGmAx=VyU&Ejl4g&G0aO4H(?3yOB+-8*U@%RO(5s|i?tpnpWY?OX|| zceC^M9rB>0kKduBN$EkmJm}M5QM#~WdE0WLRQ7C2dN#f>p{8XHu-&n?#Ey2;+L%-` zfT_bCY(;+FABGp$2sHZBu$PVknveP;5H^M+KBjiH#=RNX%Er~K_?k_$CXr_n_Oj%1 z4EYd(on{JimR7s5elYv6>ey)QE?Jr5-Pyz#?U51QEB!0nNWo%cV|^Z#+?>1KvDznj z_xs~WKhVyLHcy~!v_>VDqoG|2p+&5V+HRFp2O&^QOD?fl`O~m`NPdPS*B%HW+gcF@hDjA?$#cEx4%bvu>n|ry>Srn+2sU!^;Lh?heZ&3V-NRI1K*3V zpZL2)gt(4!AdqYn%$`D71&AVhH}-i$egXU99+`=K6fBH#0HuE$mfwBb#|d_;yU!O4oRNFH=^puHTxGvTKl~N8wbJK_jCL{961wtA#ySDGHktSPIlE01hcewQXehj zs*>^BIS&6y7+$DB3SD>KTTt-$0^4UAuBOm)>@_&z6yP;iSaabPOp8cI*9_Ood1t=h zK%GqIOXGKWoEC|&JYIJ(cPN^LPzzl+BW4zeB=FjVcMhC%L_XN? z#rm-eT{~FVHPt~ERSz!Lf*%1pFs)q?bA6)G3=Lu347lbZV(a?eyO`r>7&ARsVj~D0 z*T+y;&vHavC~BL4(iz4*c&BS4f%BHtcHU~+NZ?~BQ+_{kVxs>FYobQ{j$`$u-m#+Z znD@Z_Mm+rYcrA#_;7OUq7?r{6drN1{LQpUEXcCOYkzL%3A7_t(SP2_)eOKu$B0J~Q z??nyUI?DwoYkUj=>1>t;Az}x!He|-(avcS9LU%ukY{g`?BpYp~)$oscdsxJ(r>(cN zJ|i|bw+w9s^y%VISk_nPYcB2sTCa(4Hg}KsDH%fnUc&H`%g?x@j6Kv_F4TsW~KCLtP>*TiO>}nCs?@ayYvLqf5rs^ZyQ)I>5I%Df*a+-Ackvw4H!n)pO{)(S^a@xTZVf2YH48&<-r_j zED1%}CdjcMfhkx;ICe&wqaatzw#^JBG(!KPB$@o7VW1=56_e%CI!tJ;xrmhK8VOkm zJh)b`76;6BlZd(2^p{y&CQQa+qUAc^35-yn0YT~>m zQyWB=9o%sfLk)};VK<72sw@Eris9-Btc5KLD8Q|#Uat4ETuUC>6-m$t5Q9Nr%FaL( zqhTTkF|dH|0^%9#B;qr8N_Vm;m@5ui$E>4v%4QBYW+OcY%$~(}-kP_M+K2F7PRhwx z36z<)b2f7ntb_I#O2!?OC~EKXX^x{ITt8>=ai0T$!8B5}LYM6bZlK^Ag&=r%H`ijg zP+^TlmSL~1!FLLnnW>5X)F+9j%~jLr`M@gQ&Y)QMzNhh39O#Hct5@`YMT-7YCTFf- zNFl@FzJVDsX2PCx$=Zh>Hw^ z5y)EiTRO#2AeU(WqeR2zMe%W;#?#R75`*HLH*P4U7zYFKz0(gt6^@$-{i(!MCY|^S zGD;k=guzqvz}`q2TR;5p&inC=6jIWXf-{S6Ts{v|0!9^B?sq;F@dmDA9O(ha7Ko2Z zLM7qZX7GASytdLe3|@c88{Ej5hU?_zMf z$fV~S`#v757s%|-+=bV#C!YfV1ghJ{?|Hb1_N^DwT#o<|VAgKZj}5DqD*F%_M2+T8 z;0%vCsQQGD5)42MKptckl!(fN1K;Bk;(`(9w8JxS`{_M5yj7X^TYUekBzHP0%w7_Wo*V+*Gl7d;{};TAn^J<(fKexa$H{(ccj?<4u0yCHf*oi0TbsL) za~*>Db3c_>)-1ReZe3)N|MusQ{r1Nhy6|<7F+?luMf)FTxTAUaIppIWd|$T86fwmQwL$`*GYIsHLGpKnVOT zqo6yGl}~?Ct|Ht~5}HOC&^G8_!=P9-vgX(O!1|L|U^(^GTtTzlz0qGbuGHPHZ0&Rm0pV#3HNU?q# z;hZ-CpHe8;RPL?l$6~FaRvv%D9?Xs~6iuojl}AY<3ZA9@5WFG>Az@QDFyco+N2T{; z(u;9Ov?G+>E+17BFGg-nu76Wx6Z&Zv9JPq+451-ME*{L|@`84~WB9^za~XjmQ0O3| z>hy%6@KU&Vs&K>{$c6GHh)TE$v5KQruF)BZjk4G=xe-+0Euk!kp&aYhP@J zG@)RwHc2bZRjm_5Kn2Jnz-&^Ud3F=aOrHY*Fxh`U@oghU9;M%#Cg_0tCR|GM~Xf&$I)q((9zor!3YnI(^~NKaFC76x{j6AH`gh z!+hZ;n_Pc29X5#qrhbxSrbBoD1ZmE&Wc<&VO~cm` z&SN(Zk7^m zmg?bVtKduIX5=N}XdwDW{9pvQS*pX$Qroy$W*b++$$dv|7CvbW?8wbfH@F$|?99#j zo}-MNxfyOO_~d5bT;OKekeiWfLGQVZo8^R?<%F9JnK%5T@IQFYL$^ks-C=JMzVclz zH_{r#-Zemy55 zcY5_KL%T`#kZ{ig!*cQLN@)o)m9af--9{5zwvpb;muXzm`$+bKP`s5bv1Rk<1I%#} z73qTv-9vJSFJ!yh2>PA0#MlOXm|4EVH-5m5VeW*D9di_!BR^or&^#6Hnq#P-51kL= z^+9}OuP8k2pz}ygkR4?f%onh^l3T^CxQeR;H2v{fR9qo&4-zvCOaKc&Pdd>7k+~p* z3{XQtrD?ke@17)qYkB@<ZHI06D0!n`qs}AY$2C?iz3otPHrzQp;_?av(W# zUVdZvW(2xgoaE4fkN9b#A^5}NhDdD;AKWle6K_H2gBE#WTVG5ICqtk2*v6!vLoKn( z7X`adZN4riqNrWVy$7Wn?s5R(&LLo7M}NxOg;%KK#5LmZ5~G9P6)<|s@P`gfCqkrcn#BIhX^> zUC$W#fZ$_psw=(V_cHLK2lP`l$f65A1b1}d1xG_ux269Z$$tk4vb4G5X09mK^a(cS z51Afat$TC=zO-70pS9?+=oXxGD`gKa1PKDH(#zrZNALhZZ=t#B?23n-X|Iz8JDT{?wG1dV?5;P+SU%DF=54Im1PIzA`L4_RG`NXAik|nGiATZtz!kasL#)8(p$!%~32;iv$mA%rsJ8HP8PMtA=Q3(yzlF2_ z-YGwxkK?s?BBTaA6fQ0`H(vbG^B=DT7lj1sQh~ z0ifN5@I5}g2Ly(Qhgqe}iyS=P;-mf@3ON+BA;t68r`*Rl(plt-#A)5mK@&D+>BmqG zo7DM-T-|&9bbs;a_BYbP6#|T7l;;MdC+l)dejd(VKP7zer6PjmR6hftMDX|c#sed>Ov6cEW@omH9t>d z@;%$$?)VXdtUibCL8HNL<7@Z!FI&VL{S=?rB5~;FO!y^y$?bzV>uEeHGx`}mis?ZP zK8d5|egzbVwF^xqg0nS&Zm^OuNbQkl`raAq=A&>iJY6HJRC!*`1k9W&ba;`H|KD literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/features.cpython-39.pyc b/mplex_image/__pycache__/features.cpython-39.pyc new file mode 100755 index 0000000000000000000000000000000000000000..ed790ee4a1b4583a0bdb677a3906bd7e6eee134c GIT binary patch literal 21115 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/gating.cpython-39.pyc b/mplex_image/__pycache__/gating.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88ac253ba412d4c32c25dee9ca29dadf9da96899 GIT binary patch literal 6979 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/imagine.cpython-38.pyc b/mplex_image/__pycache__/imagine.cpython-38.pyc new file mode 100755 index 0000000000000000000000000000000000000000..49741f4fee84f06ae4c077e4cd786509f40305a5 GIT binary patch literal 11540 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 opRw&Ex6G6_PLg}I;v3IwQK zD7IDbWT)*VhfHTW)A6K39dqfa({qpg6FPg%$^U{b?(Z!~T9R#W@D}@lUF>_m_j_;o z$;^ze;kt9|Pi=lm)4r$5!OKKt4Y&9&1k;$FYM~zMBZC=xx?Z)IeJixb&ray7QC{e& zvKH1<>4$YyHo}G~XTlj(&W5un&2+9mAI|rWg~!J8EcB0u$JMiyF7{7^C-8ilIm~^i zhbNiGY7e#W6g$J}tbvx(Y=+IEM2j+V1*k)1&K52>n{fX6~Kb!m0_Nq7Ift64Ya{isOttW{p^M;mdg{9rT6 zvN&y(m+uX7vBTqANk49_wpM-{-)~)Lt$rG%J8|nm$DlRFR}H0$`vctKKM;l1(@UuH zYkg=G+Q=xO&Y@YDr8%ymbRY3P*p$S;l3DIMmV(!MgzXk@l+R@%%xqmAmNj&^hc=4Sfjpg%F?{iX-68j&mZ07xVWrOEC1A!vCpGJ4uVl(3L z%JplPKfb=qnYdZxsK(hwFJ8G6712tkfAz{re~`xa+6ftIr5_g&L(lTyzVuhqL=^{5}qx%WHUJBdgL_RwuC4IWFcn`8{jb7`h=CY@+7h%-iJO^Rks zx}q|IbVOVAproaxo=Y!H+RFB_o{FjNr8{Z$4>Z;>cB0*<2vOUXjf1B9A?^QV2+e5d zzCNqZ8YlIJ?irTu>8DX&P_6ttMm*!0HX4;@{vHqTMtx6%R~jP|r3r7-dsfdLIVF4t zzG0PCkG$*r&@SAv@>Gw?mN9gOEk_>;!)adsXU?&dr!7vEV;K3vB0!cD0stSQlp80Cjh#R)F~? zwymML8p&U!$yP`-Awo~YZRHC`ZKj|&G3fZ^pW_X&0HN6)j75g>V7gh{`~q6{n>Rag zM#LLufZvV00_L-f+xp^=Wj(w*<=nra@_f!6^D21v5U+wk$Go~$IM3x(_c^@!;h0y! z(PLgECaoRfRlnlZ(jGIZzx#b>Hwgr7GMMqu=aD%- zMspC>_;C_M(wvs&Cm@=2eukcxNSq}RkRWQ|uaHnKLtesvOyVabehSg_DhHs@q8xz# zjGkU2@j8h&NGy{${Qc6KeC;K43W^7s2mZ32;Vy%(q3;mB9W+dtt3-vEtJEH;2wxaI zlYzOI0Tx1bp%`lw8HQb&Dz|W`1Y=p0D=_nnhB0nwGOKH>YG|>iEe^G82VgRfxs-Zs zn0tD!hP=RL^=fwT+U)L!%Ffu(AyNadDr6&9QUQe^#e+ENYz7GM8C$8k0jpcr-d9sd zeVLf8xIc)Mg+f;mK>IUgzDezK>8SSwVoWE>u3kd)As3PsF)B?E-Ae@jH5Aj-Ihm&E z^S4n@x(8b1q&ry~L>y!V2juUdUYU4}dQ9X3vMzF7w7;Z`PGDl551Nj2Fq$T~NH4rc zLeZbHG|}HtBhRYuYEKkEK}MxUngy0KE4EwE7p*1TGWj}s_pN0t(V-+|auIIG;3jVI zZwO^XVWMX3X@AzbI)lj{dR3iLwg(1i4CbY5 zgmk~P02ct}YZEDF$b3p0_uoDMlaBcE+5G7#K2)ev!=E7zG9J0GAZJxM)IybGs(oo& zR+VWs^~XW?(rG%svUgOrM6ZDm67LQdIr-%uERW1RP` z>@T!0A2=R}Wo-2pl3iyrj#$hU*w&jf3XuFmEDaO`YBA&V37lf0q83E$7}=T9L

v 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/mics.cpython-39.pyc b/mplex_image/__pycache__/mics.cpython-39.pyc new file mode 100755 index 0000000000000000000000000000000000000000..68abfea99ce25897df23e2822b0e158dceaf2bf4 GIT binary patch literal 27977 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/mpimage.cpython-37.pyc b/mplex_image/__pycache__/mpimage.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..7694f6a1f5c43f30e82cb1a8ea2f5b07808ec4a7 GIT binary patch literal 27628 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 EztKT{Qvd(} literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/mpimage.cpython-39.pyc b/mplex_image/__pycache__/mpimage.cpython-39.pyc new file mode 100755 index 0000000000000000000000000000000000000000..93be7a7e34e48bacbfdba9bfa315c0b56fa1635e GIT binary patch literal 27503 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/ometiff.cpython-39.pyc b/mplex_image/__pycache__/ometiff.cpython-39.pyc new file mode 100755 index 0000000000000000000000000000000000000000..789526efe52cb7a001d8ef2da045ef1cd8beb543 GIT binary patch literal 1559 zcmbVMOK;;g5GEx{mKEDsCr);gEef3$$Yq@#ddV(|E}U$UqIsanAuFH|xF%!E(Q8S% zhz;e^v_Oyj3HGt){u>^8>B)bgmv%@yae6HUarg~2!*3oGX*7HU3X=nw0)Mg!WKr%n{Lp`(z}yvrtPo|U%0gTGYYpTzCe549N3^!Ws`n?4=blPAaOQ1 z=nV3db^1IX6#@hqC*)%!286TD*~!uOXRkRG7cvLPvi^{D4v8e4UV8k#15Vb!_BKtr z2@#@aY5&UsX+bW$U3kTN3~9i3mPAuLLw9zGhSpt0+GXV)Q|rbOufBz_?(t8!w5OFB znmUV6mdK@!4s3yj)jOxOhhFJKwbGeB>{Y0{gFxqlZmq1)>JBnC&a@85dqfRD@a1x^ z38?mn)&cpC=viseHF)a3*pIpvM6dzuH)mLTknPeLZr$TU^uzw03ny=v?hFH`hJ0SS z_xSn4ISaj0;&OTRr$3+AZa=YoK|SsHH~eWA{O!z;iF+4v@^Xfth|S3xKY-E8i*Z0Q z8l*XmugAeP&(nYujPU?U63F=a+I*LYC?))caf6g|8FoKh%^j%9g zAioa23}~FPOvHI6lyiLY?wccp_vgB>9BPQ>X436FufTLV{|Cmz?+!?ovE*R?VDB@o zHyMyDiY%GImmL*<^ww=5B=IvU0FLUQFn2$2S= z0Zpo1N4`5Lz^8wM0Un0_;PU)$_i!W&>6h literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/preprocess.cpython-37.pyc b/mplex_image/__pycache__/preprocess.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..61224bacce4f808e8a2edaa6f8285a453c6db29f GIT binary patch literal 23888 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/preprocess.cpython-38.pyc b/mplex_image/__pycache__/preprocess.cpython-38.pyc new file mode 100755 index 0000000000000000000000000000000000000000..14db79b304183a60c8aae25ac115112d1ebc19d5 GIT binary patch literal 23774 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 E0M%pCb^rhX literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/process.cpython-37.pyc b/mplex_image/__pycache__/process.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..a2ab1853d571843c17d27cf6efc250f6dc09e484 GIT binary patch literal 41042 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/process.cpython-39.pyc b/mplex_image/__pycache__/process.cpython-39.pyc new file mode 100755 index 0000000000000000000000000000000000000000..5a6c4e6fe2f0e311ea2b9c993cd5437bf2c479f1 GIT binary patch literal 42755 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 AB>(^b literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/register.cpython-37.pyc b/mplex_image/__pycache__/register.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..6b120c4d0b5940bd3f597ba06d7b138cc2b56962 GIT binary patch literal 4509 zcmcIoOK%*<5uWaO?Cg6<%6eEvGKdq)3)s6Ba|sARjviJl*rH@n$zkANFr4n)o#njL z-AihT*^^{QItWP4K>+fY{G>Sr$Vq>}Cs+6Ei;RUh2{Oy+>8gHIRd@AQ)x6zo)--tj zzVCXUZ)n;-C9!yPsQeaQ{sIc35fo`If-jD6th+jrJUudE)3sFHh$^w|+HuvbD%u3P z=GNl6TUWFdHR7h*RCN@!+?JZN?Y2pU*e^7(7x(actS1bkcZD|3DoWK?oH zmtR}1&l}@gJgdFNFY!x#fMKOz78qM44N-fAVJ@ts)=5)J&FhM4z&fs|=2C~YlGT{Y zYAto>D1LL!Z|AMNJ;1O78^=Y>foCW4nIAiGdKf0d9Ve8#;fRqhoRB*rb@r)qqX=9F4*Nx{v5w#$3cv7)5e&H6oYnGxuC3X~!SNz6eI%0i9-PmMj}$VOAwdVFhl@7A5d0#HtdFj+*CZl|zHcrL6GU5n0nh;&Ut zd2qe_Y%lzZxlPUjuY`_}Jae0bi#4()TT47~YqF`V7wF5v7^W$4FAl;8aETJ{>^v*} z*$KDK^qyHM2F~) zPchPjF*b88H(;FcMxR-#4d=RS11^l2Rg#a^xr-5FYc;0qms@#7MPAdd!J%Fcc*}f; zLabXOoOFK-5wMJP{6GbZp0h`(GZG@>JN-Tn!h=wJTDGKY*e7Yg`-C3${VeP+hAgM- zjYJ$dkXbn-91LJ%6S0!#i!e?2x?br-fJ@7GmQjxdJC3S3>0pr(D7ntZEIs6G2!lqn+(%{n)fC@vVHbo^&4k_cbYcuI# zOvZ+yRvtMo6TGOE$4=Gj%f*DV-CmapD^%3VTnXa(a9M{LN(fPTL+RaqmPJ!9)V*ceE~AM& zu*$3sxI~Ulz?f4wrf^90C8YxeDSNIzM{q!~(1mdVhx8OZhaVi~1qVhdjwP&<%BeQ1 z2uRU$Gq1>_42icrtBUGL4O(iVu38{9&uhedV?t|v)(|+a!;E!-h$R{_anG%BQ);8W zl`GnFY4(k_1^ZG-V5q%Z>a;!e@+MBfDZL@I>>%M zU5B!F%-qSclwW2g3c5|K7vas?;m$O77jz=DVe=285a7Z-QU4rOI}>zdb$M|ltrN^ z%#14DLNtMX(Z`w)Vm}jC>Y~=@RS|6sVKK zuE}ZiEE0vyC;Rg5%nK`wA!|JMIkb;Tbc2S?B7{q8=)aUS~2y5Sy!=bYW zH<^UMgdX?h4Mkz0Ld60P$m;Z~C1;;WYpyFbKtzH4XXhcnUR-Uyf?izq*AT>QWCN~k zVjIJYF2dJA4zkb%;9f*M)Wh>H`T((qFs6&(Uj_HN7YZ|(#M!h&C;id=ugkTp!Fo#r zqHv$d^qAd-0<0+`rdmuUQIkp7RC`b~7Ewn2gQ46TxfcL-Q(kU?#N2YBr3Pz;tzg`~ f5vODl(VH?!SNLrMaszh5uK_mNrtQ=&*_9svAjLE| literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/register.cpython-38.pyc b/mplex_image/__pycache__/register.cpython-38.pyc new file mode 100755 index 0000000000000000000000000000000000000000..1590041296d4e5b61ef01484e2fe3410e1fff96c GIT binary patch literal 4522 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 J-kp5*e*&?~JvRUV literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/segment.cpython-38.pyc b/mplex_image/__pycache__/segment.cpython-38.pyc new file mode 100755 index 0000000000000000000000000000000000000000..d6e2cbc8c2cbc574fef198d0e199cbb75eec24c7 GIT binary patch literal 21145 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/segment.cpython-39.pyc b/mplex_image/__pycache__/segment.cpython-39.pyc new file mode 100755 index 0000000000000000000000000000000000000000..9015372455c7274bc1196173b9476c3b9ddb9518 GIT binary patch literal 21335 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/visualize.cpython-37.pyc b/mplex_image/__pycache__/visualize.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..77489bc265fe388be7402da480bb33810fed2cac GIT binary patch literal 8994 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 literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/visualize.cpython-38.pyc b/mplex_image/__pycache__/visualize.cpython-38.pyc new file mode 100755 index 0000000000000000000000000000000000000000..4f6e1166cd07e410265da88ffc9fec2f44322eca GIT binary patch literal 12984 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 E2RO&(o&W#< literal 0 HcmV?d00001 diff --git a/mplex_image/__pycache__/visualize.cpython-39.pyc b/mplex_image/__pycache__/visualize.cpython-39.pyc new file mode 100755 index 0000000000000000000000000000000000000000..d1843c1559c04c3dfb4ddb64f18674f271bbcfc7 GIT binary patch literal 13026 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 xI0E8|7VBcoT&zg)W*l`}yi@MYg=v3 +# author: Jenny +# +# description: +# python3 library to analyze cyclic data and images after manual thresholding +#### + +#load libraries +import matplotlib as mpl +mpl.use('agg') +import pandas as pd +import numpy as np +import os +import skimage +from skimage import io +import json +from biotransistor import imagine +import itertools + +#functions +# import importlib +# importlib.reload(analyze) + +def combinations(df_tn_tumor,ls_marker=['CK19_Ring','CK7_Ring','CK5_Ring','CK14_Ring','CD44_Ring','Vim_Ring']): + ''' + get all combinations of the markers (can be overlapping) + ''' + ls_combos = [] + for i in range(0,len(ls_marker)): + for tu_combo in itertools.combinations(ls_marker,i+1):#'Ecad_Ring', + ls_combos.append(tu_combo) + + #create the combos dataframe dataframe + df_tn_counts = pd.DataFrame(index=df_tn_tumor.index) + se_all = set(ls_marker) + + #combos of 2 or more + for tu_combo in ls_combos: + print(tu_combo) + se_pos = df_tn_tumor[(df_tn_tumor.loc[:,tu_combo].sum(axis=1) ==len(tu_combo))] #those are pos + se_neg = df_tn_tumor[(df_tn_tumor.loc[:,(se_all)].sum(axis=1) == len(tu_combo))] #and only those + df_tn_counts['_'.join([item for item in tu_combo])] = df_tn_tumor.index.isin(se_pos.index.intersection(se_neg.index)) + + #other cells (negative for all) + df_tn_counts['__'] = df_tn_counts.loc[:,df_tn_counts.dtypes=='bool'].sum(axis=1)==0 + if sum(df_tn_counts.sum(axis=1)!=1) !=0: + print('error in analyze.combinations') + + return(df_tn_counts) + +def gated_combinations(df_data,ls_gate,ls_marker): + ''' + df_data = boolean cell type dataframe + ls_gate = combine each of these cell types (full coverage and non-overlapping) + ls_marker = with these cell tpyes (full coverage and non-overlapping) + ''' + es_all = set(ls_marker + ls_gate) + ls_old = df_data.columns + df_gate_counts = pd.DataFrame() + for s_gate in ls_gate: + df_tn_tumor = df_data[df_data.loc[:,s_gate]] + print(f'{s_gate} {len(df_tn_tumor)}') + #combos of 2 + if len(df_tn_tumor) >=1: + for s_marker in ls_marker: + print(s_marker) + tu_combo = (s_gate,s_marker) + es_neg = es_all - set(tu_combo) + if ~df_data.loc[:,tu_combo].all(axis=1).any(): + df_gate_counts[f"{s_gate}_{s_marker}"] = False + else: + df_gate_counts[f"{s_gate}_{s_marker}"] = df_data.loc[:,tu_combo].all(axis=1) & ~df_data.loc[:,es_neg].any(axis=1) + df_gate_counts.fillna(value=False, inplace=True) + return(df_gate_counts) + +def add_celltype(df_data, ls_cell_names, s_type_name): + ''' + add gated cell type to data frame, and save the possible cell typesand cell type name in a csv + df_data = data frame with the cell types (boolean) + ls_cell_names = list of the cell names + s_type_name = the cell category + ''' + #check cell types' exclusivity + if ((df_data.loc[:,ls_cell_names].sum(axis=1)>1)).sum()!=0: + print(f'Error in exclusive cell types: {s_type_name}') + + #make cell type object columns + for s_marker in ls_cell_names: + df_data.loc[(df_data[df_data.loc[:,s_marker]]).index,s_type_name] = s_marker + d_record = {s_type_name:ls_cell_names} + + #append the record json + if not os.path.exists('./Gating_Record.json'): + with open(f'Gating_Record.json','w') as f: + json.dump(d_record, f, indent=4, sort_keys=True) + else: + with open('Gating_Record.json','r') as f: + d_current = json.load(f) + d_current.update(d_record) + with open(f'Gating_Record.json','w') as f: + json.dump(d_current, f, indent=4, sort_keys=True) + +def thresh_meanint(df_thresh,d_crop={},s_thresh='minimum',): + """ + threshold, and output positive and negative mean intensity and array + df_thresh = dataframe of images with columns having image attributes + and index with image names, column with threshold values + d_crop = image scene and crop coordinates + + """ + d_mask = {} + for idx, s_index in enumerate(df_thresh.index): + #load image, crop, thresh + a_image = skimage.io.imread(s_index) + if len(d_crop) != 0: + tu_crop = d_crop[df_thresh.loc[s_index,'scene']] + a_image = a_image[(tu_crop[1]):(tu_crop[1]+tu_crop[3]),(tu_crop[0]):(tu_crop[0]+tu_crop[2])] + i_min = df_thresh.loc[s_index,s_thresh] + a_mask = a_image > i_min + print(f'mean positive intensity = {np.mean(a_image[a_mask])}') + df_thresh.loc[s_index,'meanpos'] = np.mean(a_image[a_mask]) + b_mask = a_image < i_min + print(f'mean negative intensity = {np.mean(a_image[b_mask])}') + df_thresh.loc[s_index,'meanneg'] = np.mean(a_image[b_mask]) + d_mask.update({s_index:a_mask}) + return(df_thresh,d_mask) + +def mask_meanint(df_img, a_mask): + ''' + for each image in dataframe of image (df_img) + calculate mean intensity in pixels in mask (a_mask) + ''' + + #for each image, calculate mean intensity in the masked area + for s_index in df_img.index: + a_img = skimage.io.imread(s_index) + a_img_total = a_img[a_mask] + i_img_meanint = a_img_total.sum()/a_img_total.size + df_img.loc[s_index,'result'] = i_img_meanint + return(df_img) + +def make_border(s_sample,df_pos,ls_color,segmentdir,savedir,b_images=True,s_find = 'Cell Segmentation Basins.tif',s_split='Scene '): + """ + load positive cells dataframe, and segmentation basins + output the borders od positive cells and the cells touching dictionary + """ + #load segmentation basins + #flattens ids into a set (stored in d_flatten) + os.chdir(segmentdir) + ls_file = os.listdir() + ls_cellseg = [] + + # list of Basin files + for s_file in ls_file: + if s_file.find(s_find)>-1: + if s_file.find(s_sample)>-1: + ls_cellseg.append(s_file) + + d_flatten = {} + dd_touch = {} + + for s_file in ls_cellseg: + s_scene_num = s_file.split(s_split)[1].split('_')[0].split(' ')[0] + print(s_file) + print(s_scene_num) + a_img = io.imread(s_file) + # get all cell ids that exist in the images + es_cell = set(a_img.flatten()) + es_cell.remove(0) + s_scene = f'scene{s_scene_num}' + d_flatten.update({f'scene{s_scene_num}':es_cell}) + + #get a cell touching dictionary (only do this one (faster)) + dd_touch.update({f'{s_sample}_{s_scene}':imagine.touching_cells(a_img, i_border_width=0)}) + + #s_type = 'Manual' + if b_images: + #save png of cell borders (single tiffs) + for idx, s_color in enumerate(ls_color): + print(f'Processing {s_color}') + #positive cells = positive cells based on thresholds + #dataframe of all the positive cells + df_color_pos = df_pos[df_pos.loc[:,s_color]] + ls_index = df_color_pos.index.tolist() + + if len(df_color_pos[(df_color_pos.scene==s_scene)])>=1: + ls_index = df_color_pos[(df_color_pos.scene==s_scene)].index.tolist() + es_cell_positive = set([int(s_index.split('cell')[-1]) for s_index in ls_index]) + + # erase all non positive basins + es_cell_negative = d_flatten[s_scene].difference(es_cell_positive) + a_pos = np.copy(a_img) + a_pos[np.isin(a_img, list(es_cell_negative))] = 0 # bue: this have to be a list, else it will not work! + + # get cell border (a_pos_border) + a_pos_border = imagine.get_border(a_pos) # border has value 1 + a_pos_border = np.uint16(a_pos_border * 65000) # border will have value 255 + #filename hack + print('saving image') + io.imsave(f'{savedir}/Registered-R{idx+100}_{s_color.replace("_",".")}.border.border.border_{df_color_pos.index[0].split("_")[0]}-{s_scene.replace("scene","Scene-")}_c2_ORG.tif',a_pos_border) + else: + print(len(df_color_pos[(df_color_pos.scene==s_scene)])) + #from elmar (reformat cells touching dictionary and save + + ddes_image = {} + for s_image, dei_image in dd_touch.items(): + des_cell = {} + for i_cell, ei_touch in dei_image.items(): + des_cell.update({str(i_cell): [str(i_touch) for i_touch in sorted(ei_touch)]}) + ddes_image.update({s_image:des_cell}) + + #save dd_touch as json file + with open(f'result_{s_sample}_cellstouching_dictionary.json','w') as f: + json.dump(ddes_image, f) + return(ddes_image) + +def make_border_all(s_sample,df_pos,segmentdir,savedir,b_images=True): + """ + load positive cells dataframe, and segmentation basins + output the borders od positive cells and the cells touching dictionary + """ + #Specify which images to save + #ls_color = df_pos.columns.tolist() + #ls_color.remove('DAPI_X') + #ls_color.remove('DAPI_Y') + #ls_color.remove('scene') + + #load segmentation basins + #flattens ids into a set (stored in d_flatten) + os.chdir(segmentdir) + ls_file = os.listdir() + ls_cellseg = [] + d_files = {} + #dictionary of file to scene ID , and a list of Basin files + for s_file in ls_file: + if s_file.find('Cell Segmentation Basins.tif')>-1: + if s_file.find(s_sample)>-1: + ls_cellseg.append(s_file) + s_scene_num = s_file.split(' ')[1] + d_files.update({f'scene{s_scene_num}':s_file}) + + d_flatten = {} + dd_touch = {} + + for s_file in ls_cellseg: + s_scene_num = s_file.split(' ')[1] + print(s_file) + a_img = skimage.io.imread(s_file) + # get all cell ids that exist in the images + es_cell = set(a_img.flatten()) + es_cell.remove(0) + s_scene = f'scene{s_scene_num}' + d_flatten.update({f'scene{s_scene_num}':es_cell}) + + #get a cell touching dictionary (only do this one (faster)) + dd_touch.update({f'{s_sample}_{s_scene}':imagine.touching_cells(a_img, i_border_width=0)}) + + #s_type = 'Manual' + if b_images: + idx=0 + #save png of all cell borders (single tiffs) + #for idx, s_color in enumerate(ls_color): + # print(f'Processing {s_color}') + #positive cells = positive cells based on thresholds + #dataframe of all the positive cells + df_color_pos = df_pos #[df_pos.loc[:,s_color]] + ls_index = df_color_pos.index.tolist() + + if len(df_color_pos[(df_color_pos.scene==s_scene)])>=1: + ls_index = df_color_pos[(df_color_pos.scene==s_scene)].index.tolist() + es_cell_positive = set([int(s_index.split('cell')[-1]) for s_index in ls_index]) + + # erase all non positive basins + es_cell_negative = d_flatten[s_scene].difference(es_cell_positive) + a_pos = np.copy(a_img) + a_pos[np.isin(a_img, list(es_cell_negative))] = 0 # bue: this have to be a list, else it will not work! + + # get cell border (a_pos_border) + a_pos_border = imagine.get_border(a_pos) # border has value 1 + a_pos_border = a_pos_border.astype(np.uint8) + a_pos_border = a_pos_border * 255 # border will have value 255 + #filename hack 2019-11-27 + skimage.io.imsave(f'{savedir}/R{idx+100}_all.all_{df_color_pos.index[0].split("_")[0]}-{s_scene.replace("scene","Scene-")}_border_c3_ORG.tif',a_pos_border) + +def celltype_to_bool(df_data, s_column): + """ + Input a dataframe and column name of cell tpyes + Output a new boolean dataframe with each col as a cell type + """ + df_bool = pd.DataFrame(index=df_data.index) + for celltype in sorted(set(df_data.loc[:,s_column])): + df_bool.loc[df_data[df_data.loc[:,s_column]==celltype].index,celltype] = True + df_bool = df_bool.fillna(value=False) + df_data.columns = [str(item) for item in df_data.columns] + return(df_bool) \ No newline at end of file diff --git a/mplex_image/cmif.py b/mplex_image/cmif.py new file mode 100755 index 0000000..62367dc --- /dev/null +++ b/mplex_image/cmif.py @@ -0,0 +1,705 @@ +# wrapper functions for cmIF image processing + +from mplex_image import preprocess, mpimage, getdata, process, features, register, ometiff +import copy +import time +import os +import numpy as np +import shutil +import subprocess +import pandas as pd +import math +from itertools import compress +import skimage +import sys +import re +from skimage import io +from skimage.util import img_as_uint +import tifffile + +#set src path (CHANGE ME) +s_src_path = '/home/groups/graylab_share/OMERO.rdsStore/engje/Data/cmIF' +s_work_path = '/home/groups/graylab_share/Chin_Lab/ChinData/Work/engje' + + +def parse_czi(czidir,type='r',b_scenes=True): + """ + parse .czi's written in koei's naming convention + type = 's' for stitched + """ + cwd = os.getcwd() + #go to directory + os.chdir(czidir) + df_img = mpimage.filename_dataframe(s_end = ".czi",s_start='R',s_split='_') + df_img['slide'] = [item[2] for item in [item.split('_') for item in df_img.index]] + if type=='s': + df_img['slide'] = [item[5] for item in [item.split('_') for item in df_img.index]] + df_img['rounds'] = [item[0] for item in [item.split('_') for item in df_img.index]] + df_img['markers'] = [item[1] for item in [item.split('_') for item in df_img.index]] + if b_scenes: + try: + df_img['scene'] = [item[1].split('.')[0] for item in [item.split('Scene-') for item in df_img.index]] + except IndexError: + print(f"{set([item[0] for item in [item.split('Scene-') for item in df_img.index]])}") + df_img['scanID'] = [item[-1].split('-Scene')[0] for item in [item.split('__') for item in df_img.index]] + os.chdir(cwd) + return(df_img) + +def parse_stitched_czi(czidir,s_slide,b_scenes=True): + ''' + parse .czi's wtitten in koei's naming convention, with periods changed to undescores + ''' + cwd = os.getcwd() + #go to directory + os.chdir(czidir) + df_img = mpimage.filename_dataframe(s_end = ".czi",s_start='R',s_split='_').rename({'data':'rounds'},axis=1) + df_img['markers'] = [item[0] for item in [item.split(f'_{s_slide}') for item in df_img.index]] + for s_index in df_img.index: + df_img.loc[s_index,'markers_un'] = df_img.loc[s_index,'markers'].split(f"{df_img.loc[s_index,'rounds']}_")[1] + df_img['markers'] = df_img.markers_un.str.replace('_','.') + df_img.slide = s_slide + if b_scenes: + df_img['scene'] = [item[1].split('-')[0] for item in [item.split('Scene-') for item in df_img.index]] + os.chdir(cwd) + return(df_img) + +def count_images(df_img): + """ + count and list slides, scenes, rounds + """ + for s_sample in sorted(set(df_img.slide)): + print(s_sample) + df_img_slide = df_img[df_img.slide==s_sample] + print('scene names') + [print(f'{item}: {sum(df_img_slide.scene==item)}') for item in sorted(set(df_img_slide.scene))] + print(f'Number of images = {len(df_img_slide)}') + print(f'Rounds:') + [print(f'{item}: {sum(df_img_slide.rounds==item)}') for item in sorted(set(df_img_slide.rounds))] + print('\n') + +def visualize_raw_images(df_img,qcdir,color='c1'): + """ + array raw images to check tissue identity, focus, etc. + """ + for s_sample in sorted(set(df_img.slide)): + print(s_sample) + + df_img_slide = df_img[df_img.slide==s_sample] + for s_scene in sorted(set(df_img_slide.scene)): + print(s_scene) + df_dapi = df_img_slide[(df_img_slide.color==color) & (df_img_slide.scene==s_scene)].sort_values(['round_ord','rounds']) + fig = mpimage.array_img(df_dapi,s_xlabel='slide',ls_ylabel=['scene','color'],s_title='rounds',tu_array=(2,len(df_dapi)//2+1),tu_fig=(24,10)) + fig.savefig(f'{qcdir}/RawImages/{s_sample}-Scene-{s_scene}_{color}_all.png') + +def registration_python(s_sample,tiffdir,regdir,qcdir): + print(f'Registering {s_sample}') + preprocess.cmif_mkdir([f'{qcdir}/RegistrationPlots/']) + os.chdir(f'{tiffdir}/{s_sample}') + df_img = mpimage.parse_org(s_end = "ORG.tif",type='raw') + df_img['round_ord'] = [int(re.sub('[^0-9]','', item)) for item in df_img.rounds] + df_img = df_img.sort_values(['round_ord','rounds','color','scene']) + for i_scene in sorted(set(df_img.scene)): + preprocess.cmif_mkdir([f'{regdir}/{s_sample}-Scene-{i_scene}']) + df_dapi = df_img[(df_img.color=='c1') & (df_img.scene==i_scene)] + target_file = df_dapi[df_dapi.rounds=='R1'].index[0] + target = io.imread(target_file) + for moving_file in df_dapi.index: + s_round = moving_file.split('_')[0] + moving_pts, target_pts, transformer = register.register(target_file,moving_file,b_plot=True) + for moving_channel in df_img[(df_img.rounds==s_round) & (df_img.scene==i_scene)].index: + moving = io.imread(moving_channel) + warped_img, warped_pts = register.apply_transform(moving, target, moving_pts, target_pts, transformer) + warped_img = img_as_uint(warped_img) + io.imsave(f"{regdir}/{s_sample}-Scene-{i_scene}/Registered-{moving_channel.split(s_sample)[0]}{s_sample}-Scene-{moving_channel.split('-Scene-')[1]}",warped_img) + +def run_registration_matlab(d_register, ls_order, tiffdir, regdir, N_colors='5'): + """ + run registration on server with or without cropping + """ + os.chdir(tiffdir) + shutil.copyfile(f'{s_src_path}/src/wrapper.sh', './wrapper.sh') + for s_sample, d_crop in d_register.items(): + if len(d_crop) > 0: + print(f'Large registration {s_sample}') + for key, value in d_crop.items(): + if len(str(key)) == 1: + preprocess.cmif_mkdir([f'{regdir}/{s_sample.split("-Scene")[0]}-Scene-00{str(key)}']) + elif len(str(key)) == 2: + preprocess.cmif_mkdir([f'{regdir}/{s_sample.split("-Scene")[0]}-Scene-0{str(key)}']) + preprocess.large_registration_matlab(N_smpl='10000',N_colors=N_colors,s_rootdir=tiffdir, s_subdirname=regdir, + d_crop_regions=d_crop, s_ref_id='./R1_*_c1_ORG.tif', ls_order=ls_order) + MyOut = subprocess.Popen(['sbatch', 'wrapper.sh'], #the script runs fine + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + #regular registration + else: + print(f'Regular registration {s_sample}') + df_img = mpimage.parse_org(s_end = "ORG.tif",type='raw') + df_img['slide_scene'] = df_img.slide + '-Scene-' + df_img.scene + preprocess.cmif_mkdir([(f'{regdir}/{item}') for item in sorted(set(df_img.slide_scene))]) #this will break with diff slides + preprocess.registration_matlab(N_smpl='10000',N_colors=N_colors,s_rootdir=tiffdir, s_subdirname=f'{regdir}/', + s_ref_id='./R1_*_c1_ORG.tif',ls_order =ls_order) + MyOut = subprocess.Popen(['sbatch', 'wrapper.sh'], #the script runs fine + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + +def visualize_reg_images(regdir,qcdir,color='c1',s_sample=''): + """ + array registered images to check tissue identity, focus, etc. + """ + #check registration + preprocess.cmif_mkdir([f'{qcdir}/RegisteredImages']) + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + if s_dir.find(s_sample) > -1: + os.chdir(s_dir) + s_sample_name = s_dir.split('-Scene')[0] + print(s_sample_name) + df_img = mpimage.parse_org(s_end = "ORG.tif",type='reg') + ls_scene = sorted(set(df_img.scene)) + for s_scene in ls_scene: + print(s_scene) + df_img_scene = df_img[df_img.scene == s_scene] + df_img_stain = df_img_scene[df_img_scene.color==color] + df_img_sort = df_img_stain.sort_values(['round_ord','rounds']) + i_sqrt = math.ceil(math.sqrt(len(df_img_sort))) + fig = mpimage.array_img(df_img_sort,s_xlabel='marker',ls_ylabel=['scene','color'],s_title='rounds',tu_array=(2,len(df_img_sort)//2+1),tu_fig=(24,10)) + #fig = mpimage.array_img(df_img_sort,s_column='color',s_row='rounds',s_label='scene',tu_array=(i_sqrt,i_sqrt),tu_fig=(16,14)) + fig.savefig(f'{qcdir}/RegisteredImages/{s_scene}_registered_{color}.png') + os.chdir('..') + return(df_img_sort) + +def rename_files(d_rename,dir,b_test=True): + """ + change file names + """ + os.chdir(dir) + for idx, s_dir in enumerate(sorted(os.listdir())): + s_path = f'{dir}/{s_dir}' + os.chdir(s_path) + #s_sample = s_dir.split('-Scene')[0] + print(s_dir) + df_img = mpimage.parse_org(s_end = "ORG.tif",type='reg') + es_wrong= preprocess.check_names(df_img) + if b_test: + print('This is a test') + preprocess.dchange_fname(d_rename,b_test=True) + elif b_test==False: + print('Changing name - not a test') + preprocess.dchange_fname(d_rename,b_test=False) + else: + pass + +def autofluorescence_subtract_dir(regdir,codedir,d_channel,ls_exclude,subdir,d_early={}): + ''' + AF subtract images + ''' + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + print(s_dir) + s_path = f'{regdir}/{s_dir}' + os.chdir(s_path) + #preprocess.cmif_mkdir([f'{s_path}/AFSubtracted']) + s_sample = s_dir.split('-Scene')[0] + df_img = mpimage.parse_org(s_end = "ORG.tif",type='reg') + #load exposure times csv + df_exp = pd.read_csv(f'{codedir}/{s_sample}_ExposureTimes.csv',index_col=0,header=0)# + #AF subtract images + df_img_exp = mpimage.add_exposure(df_img,df_exp,type='czi') + if len(d_early)>0: + df_markers, df_copy = mpimage.subtract_scaled_images(df_img_exp,d_late=d_channel, + d_early=d_early, ls_exclude=ls_exclude,subdir=subdir,b_8bit=False) + else: + df_markers, df_copy = mpimage.subtract_images(df_img_exp,d_channel=d_channel, + ls_exclude=ls_exclude,subdir=subdir,b_8bit=False) + + return(df_markers) + +def autofluorescence_subtract(s_sample,df_img,codedir,d_channel,ls_exclude,subdir,d_early={}): + ''' + AF subtract images + ''' + df_img = mpimage.parse_org(s_end = "ORG.tif",type='reg') + #load exposure times csv + df_exp = pd.read_csv(f'{codedir}/{s_sample}_ExposureTimes.csv',index_col=0,header=0)# + #AF subtract images + df_img_exp = mpimage.add_exposure(df_img,df_exp,type='czi') + if len(d_early)>0: + df_markers, df_copy = mpimage.subtract_scaled_images(df_img_exp,d_late=d_channel, + d_early=d_early, ls_exclude=ls_exclude,subdir=subdir,b_8bit=False) + else: + df_markers, df_copy = mpimage.subtract_images(df_img_exp,d_channel=d_channel, + ls_exclude=ls_exclude,subdir=subdir,b_8bit=False) + + return(df_markers) + +def multipage_ome_tiff(d_combos,d_crop,tu_dim,s_dapi,regdir,b_crop=False): + ''' + make custom overlays, either original of AF subtracted, save at 8 bit for size, and thresholding + ''' + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + print(s_dir) + s_path = f'{regdir}/{s_dir}' + os.chdir(s_path) + df_img = mpimage.parse_org(s_end = "ORG.tif",s_start='R',type='reg') + df_dapi = df_img[df_img.marker.str.contains(s_dapi.split('_')[0])] + df_img_stain = df_img[(~df_img.marker.str.contains('DAPI'))] + #check + es_test = set() + for key, item in d_combos.items(): + es_test = es_test.union(item) + print(set(df_img_stain.marker) - es_test) + + #cropped + if b_crop: + s_scene = set(d_crop).intersection(set(df_img.scene)) + d_crop_scene={k: d_crop[k] for k in (sorted(s_scene))} + process.custom_crop_overlays(d_combos,d_crop_scene, df_img,s_dapi, tu_dim=tu_dim) #df_dapi, + else: + process.custom_overlays(d_combos, df_img_stain, df_dapi) + +def visualize_multicolor_overlay(s_scene,subdir,qcdir,d_overlay,d_crop,es_bright,high_thresh): + s_sample = s_scene.split('-Scene')[0] + preprocess.cmif_mkdir([f'{qcdir}/{s_sample}']) + if os.path.exists(f'{subdir}/{s_sample}'): + s_path = f'{subdir}/{s_sample}' + elif os.path.exists(f'{subdir}/{s_scene}'): + s_path = f'{subdir}/{s_scene}' + os.chdir(s_path) + df_img = mpimage.parse_org() + df_img['path'] = [f'{s_path}/{item}' for item in df_img.index] + df_dapi_round = df_img[(df_img.color=='c1')&(df_img.scene==s_scene) & (df_img.rounds=='R2')] + df_scene = df_img[(df_img.color!='c1') & (df_img.scene==s_scene)] + for s_round,ls_marker in d_overlay.items(): + print(f'Generating multicolor overlay {[item for item in ls_marker]}') + df_round = df_scene[df_scene.marker.isin(ls_marker)] + high_thresh=0.999 + d_overlay_round = {s_round:ls_marker} + d_result = mpimage.multicolor_png(df_round,df_dapi_round,s_scene=s_scene,d_overlay=d_overlay_round,d_crop=d_crop,es_dim={'nada'},es_bright=es_bright,low_thresh=2000,high_thresh=high_thresh) + for key, tu_result in d_result.items(): + io.imsave(f'{qcdir}/{s_sample}/ColorArray_{s_scene}_{key}_{".".join(tu_result[0])}.png',tu_result[1]) + +def cropped_ometiff(s_scene,subdir,cropdir,d_crop,d_combos,s_dapi,tu_dim,b_8bit=True): + s_sample = s_scene.split('-Scene')[0] + if os.path.exists(f'{subdir}/{s_sample}'): + os.chdir(f'{subdir}/{s_sample}') + elif os.path.exists(f'{subdir}/{s_scene}'): + os.chdir(f'{subdir}/{s_scene}') + df_img = mpimage.parse_org() + d_crop_scene = {s_scene:d_crop[s_scene]} + if b_8bit: + dd_result = mpimage.overlay_crop(d_combos,d_crop_scene,df_img,s_dapi,tu_dim) + else: + dd_result = mpimage.overlay_crop(d_combos,d_crop_scene,df_img,s_dapi,tu_dim,b_8bit=False) + for s_crop, d_result in dd_result.items(): + for s_type, (ls_marker, array) in d_result.items(): + print(f'Generating multi-page ome-tiff {[item for item in ls_marker]}') + new_array = array[np.newaxis,np.newaxis,:] + s_xml = ometiff.gen_xml(new_array, ls_marker) + with tifffile.TiffWriter(f'{cropdir}/{s_crop}_{s_type}.ome.tif') as tif: + tif.save(new_array, photometric = "minisblack", description=s_xml, metadata = None) + +def crop_registered(s_scene,bigdir,regdir,d_crop): + ''' + crop a stack of tiffs to the specified coordinates + d_crop: crop to scene:(xmin, y_min, xmax, ymax) + ''' + s_sample = s_scene.split('-Scene')[0] + print(s_scene) + os.chdir(f'{bigdir}/{s_scene}') + df_img = mpimage.parse_org() + df_scene = df_img[df_img.scene==s_scene] + for s_image in df_scene.index: + #print(s_image) + a_dapi = io.imread(s_image) + for idx, xy_cropcoor in d_crop.items(): + #crop + a_crop = a_dapi[xy_cropcoor[1]:xy_cropcoor[3],xy_cropcoor[0]:xy_cropcoor[2]] + preprocess.cmif_mkdir([f'{regdir}/{s_sample}-Scene-{idx:03}']) + io.imsave(f'{regdir}/{s_sample}-Scene-{idx:03}/{s_image.replace(s_scene,f"{s_sample}-Scene-{idx:03}")}',a_crop,check_contrast=False) + +def multipage_tiff(d_combos,d_crop,tu_dim,s_dapi,regdir,b_crop=False): + ''' + make custom overlays, either original of AF subtracted, save at 8 bit for size, and thresholding + ''' + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + print(s_dir) + s_path = f'{regdir}/{s_dir}' + os.chdir(s_path) + df_img = mpimage.parse_org(s_end = "ORG.tif",s_start='R',type='reg') + df_dapi = df_img[df_img.marker.str.contains(s_dapi.split('_')[0])] + df_img_stain = df_img[(~df_img.marker.str.contains('DAPI'))] + #check + es_test = set() + for key, item in d_combos.items(): + es_test = es_test.union(item) + print(set(df_img_stain.marker) - es_test) + + #cropped + if b_crop: + s_scene = set(d_crop).intersection(set(df_img.scene)) + d_crop_scene={k: d_crop[k] for k in (sorted(s_scene))} + process.custom_crop_overlays(d_combos,d_crop_scene, df_img,s_dapi, tu_dim=tu_dim) #df_dapi, + else: + process.custom_overlays(d_combos, df_img_stain, df_dapi) + +def crop_basins(d_crop,tu_dim,segdir,cropdir,s_type='Cell'): + """ + crop the segmentation basins (cell of nuceli) to same coord as images for veiwing in Napari + """ + cwd = os.getcwd() + for s_scene, xy_cropcoor in d_crop.items(): + print(s_scene) + s_sample = s_scene.split('-Scene-')[0] + os.chdir(f'{segdir}/{s_sample}_Segmentation/') + + for s_file in os.listdir(): + if s_file.find(f'{s_type} Segmentation Basins.tif') > -1: #Nuclei Segmentation Basins.tif #Cell Segmentation Basins.tif + if s_file.find(s_scene.split('-Scene-')[1]) > -1: + a_seg = skimage.io.imread(s_file) + a_crop = a_seg[(xy_cropcoor[1]):(xy_cropcoor[1]+tu_dim[1]),(xy_cropcoor[0]):(xy_cropcoor[0]+tu_dim[0])] + s_coor = f'x{xy_cropcoor[0]}y{xy_cropcoor[1]}.tif' + #crop file + s_file_new = f'{cropdir}/{s_sample}-{s_file.replace(" - ","_").replace(" ","").replace("Scene","Scene-").replace(".tif",s_coor)}' + print(s_file_new) + skimage.io.imsave(s_file_new,a_crop) + os.chdir(cwd) + +def load_crop_labels(d_crop,tu_dim,segdir,cropdir,s_find='Nuclei Segmentation Basins'): + """ + crop the segmentation basins (cell of nuceli) to same coord as images for veiwing in Napari + s_find: 'exp5_CellSegmentationBasins' or 'Nuclei Segmentation Basins' + """ + cwd = os.getcwd() + for s_scene, xy_cropcoor in d_crop.items(): + print(s_scene) + s_sample = s_scene.split('-Scene-')[0] + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation/') + + for s_file in os.listdir(): + if s_file.find(s_find) > -1: #Nuclei Segmentation Basins.tif #Cell Segmentation Basins.tif + if s_file.find(s_scene.split(s_sample)[1]) > -1: + a_seg = skimage.io.imread(s_file) + a_crop = a_seg[(xy_cropcoor[1]):(xy_cropcoor[1]+tu_dim[1]),(xy_cropcoor[0]):(xy_cropcoor[0]+tu_dim[0])] + s_coor = f'x{xy_cropcoor[0]}y{xy_cropcoor[1]}.tif' + #crop file + s_file_new = f'{cropdir}/{s_file.replace(" ","").replace(".tif",s_coor)}' + print(s_file_new) + skimage.io.imsave(s_file_new,a_crop) + os.chdir(cwd) + +def load_labels(d_crop,segdir,s_find='Nuclei Segmentation Basins'): + """ + load the segmentation basins (cell of nuceli) + s_find: 'exp5_CellSegmentationBasins' or 'Nuclei Segmentation Basins' + """ + d_label={} + cwd = os.getcwd() + for s_scene, xy_cropcoor in d_crop.items(): + print(s_scene) + s_sample = s_scene.split('-Scene-')[0] + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation/') + for s_file in os.listdir(): + if s_file.find(s_find) > -1: #Nuclei Segmentation Basins.tif #Cell Segmentation Basins.tif + if s_file.find(s_scene.split(s_sample)[1]) > -1: + a_seg = skimage.io.imread(s_file) + d_label.update({s_scene:a_seg}) + os.chdir(cwd) + return(d_label) + +def crop_labels(d_crop,d_label,tu_dim,cropdir,s_name='Nuclei Segmentation Basins'): + """ + crop the segmentation basins (cell of nuceli) to same coord as images for veiwing in Napari + s_name = + """ + for s_scene, xy_cropcoor in d_crop.items(): + print(s_scene) + a_seg = d_label[s_scene] + a_crop = a_seg[(xy_cropcoor[1]):(xy_cropcoor[1]+tu_dim[1]),(xy_cropcoor[0]):(xy_cropcoor[0]+tu_dim[0])] + s_coor = f'x{xy_cropcoor[0]}y{xy_cropcoor[1]}.tif' + #crop file + s_file_new = f'{cropdir}/{s_name.replace(" ","").replace(".tif",s_coor)}' + print(s_file_new) + skimage.io.imsave(s_file_new,a_crop) + + +#### OLD: for Guillaume's pipeline ### + +def copy_files(dir,dapi_copy, marker_copy,b_test=True): + """ + copy and rename files if needed as dummies + """ + os.chdir(dir) + for idx, s_dir in enumerate(sorted(os.listdir())): + s_path = f'{dir}/{s_dir}' + os.chdir(s_path) + s_sample = s_dir.split('-Scene')[0] + df_img = mpimage.parse_org(s_end = "ORG.tif") + print(s_dir) + if b_test: + for key, dapi_item in dapi_copy.items(): + preprocess.copy_dapis(s_r_old=key,s_r_new=f'-R{dapi_item}_',s_c_old='_c1_',s_c_new='_c2_',s_find='_c1_ORG.tif',b_test=True) + i_count=0 + for idx,(key, item) in enumerate(marker_copy.items()): + preprocess.copy_markers(df_img, s_original=key, ls_copy = item,i_last_round= dapi_item + i_count, b_test=True) + i_count=i_count + len(item) + elif b_test==False: + print('Changing name - not a test') + for key, dapi_item in dapi_copy.items(): + preprocess.copy_dapis(s_r_old=key,s_r_new=f'-R{dapi_item}_',s_c_old='_c1_',s_c_new='_c2_',s_find='_c1_ORG.tif',b_test=False) + i_count=0 + for idx,(key, item) in enumerate(marker_copy.items()): + preprocess.copy_markers(df_img, s_original=key, ls_copy = item,i_last_round= dapi_item + i_count, b_test=False) + i_count=i_count + len(item) + else: + pass + +def segmentation_thresholds(regdir,qcdir, d_segment): + """ + visualize binary mask of segmentaiton threholds + """ + preprocess.cmif_mkdir([f'{qcdir}/Segmentation']) + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + s_path = f'{regdir}/{s_dir}' + os.chdir(s_path) + df_img = mpimage.parse_org(s_end = "ORG.tif",type='reg') + s_sample = s_dir.split('-Scene')[0] + print(s_sample) + if (len(set(df_img.scene))) < 3: + d_seg = preprocess.check_seg_markers(df_img,d_segment, i_rows=1, t_figsize=(10,6)) #few scenes + elif (len(set(df_img.scene))) > 8: + d_seg = preprocess.check_seg_markers(df_img,d_segment, i_rows=3, t_figsize=(10,6)) #more scenes + else: + d_seg = preprocess.check_seg_markers(df_img,d_segment, i_rows=2, t_figsize=(10,6)) #more scenes + for key, fig in d_seg.items(): + fig.savefig(f'{qcdir}/Segmentation/{s_dir}_{key}_segmentation.png') + +def move_af_img(s_sample, regdir, subdir, dirtype='tma',b_move=False): + ''' + dirtype = 'single' or 'tma' or 'unsub' + ''' + #move + os.chdir(regdir) + for s_dir in sorted(os.listdir()): + if s_dir.find(s_sample)>-1: + if dirtype =='single': + preprocess.cmif_mkdir([f'{subdir}/{s_dir}']) + elif dirtype == 'tma': + preprocess.cmif_mkdir([f'{subdir}/{s_sample}']) + elif dirtype == 'unsub': + preprocess.cmif_mkdir([f'{subdir}/{s_sample}']) + if dirtype != 'unsub': + print(f'{regdir}/{s_dir}/AFSubtracted') + os.chdir(f'{regdir}/{s_dir}/AFSubtracted') + else: + os.chdir(f'{regdir}/{s_dir}') + for s_file in sorted(os.listdir()): + if dirtype =='single': + movedir = f'{subdir}/{s_dir}/{s_file}' + print(f'{regdir}/{s_dir}/AFSubtracted/{s_file} moved to {movedir}') + elif dirtype == 'tma': + movedir = f'{subdir}/{s_sample}/{s_file}' + print(f'{regdir}/{s_dir}/AFSubtracted/{s_file} moved to {movedir}') + elif dirtype == 'unsub': + movedir = f'{subdir}/{s_sample}/{s_file}' + print(f'{regdir}/{s_dir}/{s_file} moved to {movedir}') + if b_move: + if dirtype != 'unsub': + shutil.move(f'{regdir}/{s_dir}/AFSubtracted/{s_file}', f'{movedir}') + else: + shutil.move(f'{regdir}/{s_dir}/{s_file}', f'{movedir}') + +def extract_dataframe(s_sample, segdir,qcdir,i_rows=1): + ''' + get mean intensity, centroid dataframes + ''' + preprocess.cmif_mkdir([f'{qcdir}/Segmentation']) + #get data + os.chdir(segdir) + dd_run = getdata.get_df(s_folder_regex=f"^{s_sample}.*_Features$",es_value_label = {"MeanIntensity","CentroidY","CentroidX"})# + os.chdir(f'{s_sample}_Segmentation') + d_reg = process.check_seg(s_sample=s_sample,ls_find=['Cell Segmentation Full Color'], i_rows=i_rows, t_figsize=(8,8))# + for key, item in d_reg.items(): + item.savefig(f'{qcdir}/Segmentation/FullColor_{key}.png') + +def metadata_table(regdir,segdir): + """ + output channel/marker mapping + """ + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + s_path = f'{regdir}/{s_dir}' + os.chdir(s_path) + df_img = mpimage.parse_org(s_end = "ORG.tif",type='reg') + if len(set(df_img.scene)) > 1: + df_img = df_img[df_img.scene==sorted(set(df_img.scene))[1]] + s_sample = s_dir + else: + s_sample = s_dir.split('-Scene')[0] + print(s_sample) + df_marker = df_img[df_img.color!='c1'] + df_marker = df_marker.sort_values(['rounds','color']) + df_dapi = pd.DataFrame(index = [df_marker.marker.tolist()],columns=['rounds','colors','minimum','maximum','exposure','refexp','location']) + df_dapi['rounds'] = df_marker.loc[:,['rounds']].values + df_dapi['colors'] = df_marker.loc[:,['color']].values + df_dapi['minimum'] = 1003 + df_dapi['maximum'] = 65535 + df_dapi['exposure'] = 100 + df_dapi['refexp'] = 100 + df_dapi['location'] = 'All' + df_dapi.to_csv(f'{segdir}/metadata_{s_sample}_RoundsCyclesTable.csv',header=True) + +def segmentation_inputs(regdir,segdir, d_segment,tma_bool=False,b_start=False,i_counter=0,b_java=False): + """ + make inputs for guillaumes segmentation + """ + + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + s_path = f'{regdir}/{s_dir}' + os.chdir(s_path) + df_img = mpimage.parse_org(s_end = "ORG.tif",type='reg') + if len(set(df_img.scene)) > 1: + df_img = df_img[df_img.scene==sorted(set(df_img.scene))[1]] + s_sample = s_dir + else: + s_sample = s_dir.split('-Scene')[0] + print(s_sample) + df_marker = df_img[df_img.color!='c1'] + df_marker = df_marker.sort_values(['rounds','color']) + df_dapi = pd.DataFrame(index = [df_marker.marker.tolist()],columns=['rounds','colors','minimum','maximum','exposure','refexp','location']) + df_dapi['rounds'] = df_marker.loc[:,['rounds']].values + df_dapi['colors'] = df_marker.loc[:,['color']].values + df_dapi['minimum'] = 1003 + df_dapi['maximum'] = 65535 + df_dapi['exposure'] = 100 + df_dapi['refexp'] = 100 + df_dapi['location'] = 'All' + for s_key,i_item in d_segment.items(): + df_dapi.loc[s_key,'minimum'] = i_item + df_dapi.to_csv(f'{segdir}/metadata_{s_sample}_RoundsCyclesTable.csv',header=True) + #create cluster.java file + if b_java: + df_dapi.to_csv('RoundsCyclesTable.txt',sep=' ',header=False) + preprocess.cluster_java(s_dir=f'JE{idx + i_counter}',s_sample=s_sample,imagedir=f'{s_path}',segmentdir=segdir,type='exacloud',b_segment=True,b_TMA=tma_bool) + if b_start: + os.chdir(f'{s_work_path}/exacloud/JE{idx}') #exacloud + #shutil.copyfile(f'{s_src_path}/src/javawrapper.sh', './javawrapper.sh') + print(f'JE{idx + i_counter}') + subprocess.run(["make"]) + subprocess.run(["make", "slurm"]) + +def prepare_dataframe(s_sample,ls_dapi,dapi_thresh,d_channel,ls_exclude,segdir,codedir,s_af='none', b_afsub=False): + ''' + filter data by last dapi, standard location, subtract AF, output treshold csv + ls_dapi[0] becomes s_dapi + ''' + + os.chdir(f'{segdir}') + #load data + df_mi = process.load_mi(s_sample) + df_xy = process.load_xy(s_sample) + #drop extra centroid columns,add scene column + df_xy = df_xy.loc[:,['DAPI_X','DAPI_Y']] + df_xy = process.add_scene(df_xy) + df_xy.to_csv(f'features_{s_sample}_CentroidXY.csv') + #filter by last DAPI + df_dapi_mi = process.filter_dapi(df_mi,df_xy,ls_dapi[0],dapi_thresh,b_images=True) + + #filter mean intensity by biomarker location in metadata + df_filter_mi, es_standard = process.filter_standard(df_dapi_mi,d_channel,s_dapi=ls_dapi[0]) + + df_filter_mi.to_csv(f'features_{s_sample}_FilteredMeanIntensity_{ls_dapi[0]}{dapi_thresh}.csv') + #background qunatiles + ''' + df_bg = process.filter_background(df_mi, es_standard) + df_bg.to_csv(f'features_{s_sample}_BackgroundQuantiles.csv') + df_bg = process.filter_background(df_dapi_mi, es_standard) + df_bg.to_csv(f'features_{s_sample}_FilteredBackgroundQuantiles.csv') + + df_t = pd.read_csv(f'metadata_{s_sample}_RoundsCyclesTable.csv',index_col=0,header=0) + df_exp = pd.read_csv(f'{codedir}/{s_sample}_ExposureTimes.csv',index_col=0,header=0) + df_tt = process.add_exposure_roundscyles(df_t, df_exp,es_standard, ls_dapi = ls_dapi) + df_tt.to_csv(f'metadata_{s_sample}_RoundsCyclesTable_ExposureTimes.csv') + if b_afsub: + #load metadata + df_t = pd.read_csv(f'metadata_{s_sample}_RoundsCyclesTable_ExposureTimes.csv',index_col=0,header=0) + #normalize by exposure time, and save to csv + lb_columns = [len(set([item]).intersection(set(df_t.index)))>0 for item in [item.split('_')[0] for item in df_filter_mi.columns]] + df_filter_mi = df_filter_mi.loc[:,lb_columns] + df_norm = process.exposure_norm(df_filter_mi,df_t) + df_norm.to_csv(f'features_{s_sample}_ExpNormalizedMeanIntensity_{ls_dapi[0]}{dapi_thresh}.csv') + #subtract AF channels in data + df_sub,ls_sub,ls_record = process.af_subtract(df_norm,df_t,d_channel,ls_exclude) + df_out = process.output_subtract(df_sub,df_t) + df_sub.to_csv(f'features_{s_sample}_AFSubtractedMeanIntensityNegative{s_af}_{ls_dapi[0]}{dapi_thresh}.csv') + df_out.to_csv(f'features_{s_sample}_AFSubtractedMeanIntensity{s_af}_{ls_dapi[0]}{dapi_thresh}.csv') + f = open(f"{s_sample}_AFsubtractionData_{s_af}.txt", "w") + f.writelines(ls_record) + f.close() + else: + df_out = df_filter_mi + #output thresholding csv + #df_out = process.add_scene(df_out) #df_out + #df_thresh = process.make_thresh_df(df_out,ls_drop=None) + #df_thresh.to_csv(f'thresh_XX_{s_sample}.csv') + ''' + print('Done') + +def fetch_celllabel(s_sampleset, s_slide, s_ipath, s_opath = './', es_scene = None, es_filename_endswith ={'Cell Segmentation Basins.tif', 'Nuclei Segmentation Basins.tif'}, s_sep = ' - ', b_test=True): + ''' + input: + s_sampleset: sample set name. e.g. jptma + s_slide: slide name. e.g. jp-tma1-1 + es_scene: set of scenes of interest. The scenes have to be written in the same way as in the basin file name. + if None, all scenes are if interest. default is None. + s_ipath: absolute or relative path where the basin files can be found. + s_opath: path to where the fetched basin files should be outputed. + a folder, based on the s_sampleset, will be generated (if it not already exist), where the basin files will be placed. + es_filename_endswith: set of patters that defind the endings of the files of interest. + s_sep: separator to separate slide and scenes in the file name. + b_test: test flag. if True no files will be copied, it is just a simulation mode. + + output: + folder with basin flies. placed at {s_opath}{s_sampleset}_segmentation_basin/ + + description: + fetches basin (cell label) files from Guillaume's segmentation pipeline + and copies them into a folder at s_opath, named according to s_sampleset name. + ''' + # generate output directory + os.makedirs('{}{}_segmentation_basin/'.format(s_opath, s_sampleset), exist_ok=True) + # processing + if (es_scene is None): + i_total = 'all' + else: + i_total = len(es_scene) * len(es_filename_endswith) + es_sanity_scene = copy.deepcopy(es_scene) + i = 0 + for s_file in sorted(os.listdir(s_ipath)): + # check for file of interest + b_flag = False + for s_filename_endswith in es_filename_endswith: + if (s_file.endswith(s_filename_endswith)): + if (es_scene is None): + b_flag = True + break + else: + for s_scene in es_scene: + if (s_file.startswith(s_scene + s_sep)): + es_sanity_scene.discard(s_scene) + b_flag = True + break + break + # copy file + if (b_flag): + i += 1 + print('copy {}/{}: {}{}{} ...'.format(i, i_total, s_slide, s_sep, s_file)) + if not (b_test): + shutil.copyfile(src='{}{}'.format(s_ipath, s_file), dst='{}{}_segmentation_basin/{}{}{}'.format(s_opath, s_sampleset, s_slide, s_sep, s_file)) + # sanity check + if not (es_scene is None) and (i != i_total): + sys.exit('Error: no file found for es_scene specified scene {}'.format(sorted(es_sanity_scene))) \ No newline at end of file diff --git a/mplex_image/codex.py b/mplex_image/codex.py new file mode 100755 index 0000000..a67c58a --- /dev/null +++ b/mplex_image/codex.py @@ -0,0 +1,452 @@ +# wrapper functions for codex image processing + +#from mplex_image import preprocess, mpimage, process, +from mplex_image import features +import os +import pandas as pd +import math +import skimage +from skimage import io, filters +import re +import numpy as np + +def parse_img(s_end = ".tif",s_start='reg'): + """ + This function will parse images following akoya stiched naming convention + """ + s_path = os.getcwd() + ls_file = [] + for file in os.listdir(): + if file.endswith(s_end): + if file.find(s_start)==0: + ls_file = ls_file + [file] + df_img = pd.DataFrame(index=ls_file) + df_img['rounds'] = [item.split('_')[1].split('cyc')[1] for item in df_img.index] + df_img['color'] = [item.split('_')[3] for item in df_img.index] + df_img['slide'] = [item.split('_')[0] for item in df_img.index] + df_img['marker'] = [item.split('_')[-1].split('.')[0] for item in df_img.index] + df_img['marker_string'] = [item.split('_')[-1].split('.')[0] for item in df_img.index] + df_img['path'] = [f"{s_path}/{item}" for item in df_img.index] + return(df_img) + +def load_li(ls_sample): + ''' + load threshold on the segmentation marker images acquired during feature extraction + ''' + df_img_all =pd.DataFrame() + for s_sample in ls_sample: + df_img = pd.read_csv(f'thresh_{s_sample}_ThresholdLi.csv', index_col=0) + df_img['rounds'] = [item.split('_')[1].split('cyc')[1] for item in df_img.index] + df_img['color'] = [item.split('_')[3] for item in df_img.index] + df_img['slide'] = s_sample + df_img['scene'] = [item.split('_')[0].split('reg')[1] for item in df_img.index] + df_img['marker'] = [item.split('_')[-1].split('.')[0] for item in df_img.index] #parse file name for biomarker + df_img['slide_scene'] = df_img.slide + '_scene' + df_img.scene + df_img_all = df_img_all.append(df_img) + return(df_img_all) + +def underscore_to_dash(df_mi_full,df_img_all): + ''' + the underscore in sample names will break downstream code; change to dash + ''' + #naming underscore to dash + df_mi_full['slide'] = [item.split('_scene')[0].replace('_','-') for item in df_mi_full.index] + df_mi_full.index = [f"_scene{item.split('_scene')[1]}" for item in df_mi_full.index] + df_mi_full.index = df_mi_full.slide + df_mi_full.index + df_mi_full['scene'] = [item.split('_')[1] for item in df_mi_full.index] + df_mi_full['slide_scene'] = df_mi_full.slide + '_' + df_mi_full.scene + #df_img renameing + df_img_all['slide'] = [item.replace('_','-') for item in df_img_all.slide] + df_img_all['slide_scene'] = df_img_all.slide + '_scene' + df_img_all.scene + return(df_mi_full,df_img_all) + +def extract_cellpose_features(s_sample, segdir, subdir, ls_seg_markers, nuc_diam, cell_diam,s_scene='reg001'): + ''' + load the segmentation results, the input images, and the channels images + extract mean intensity from each image, and centroid, area and eccentricity for + ''' + + df_sample = pd.DataFrame() + df_thresh = pd.DataFrame() + if os.path.exists(f'{segdir}/{s_scene}Cellpose_Segmentation'): + os.chdir(f'{segdir}/{s_scene}Cellpose_Segmentation') + else: + os.chdir(f'{segdir}') + ls_scene = [] + d_match = {} + for s_file in os.listdir(): + if s_file.find(f'{".".join(ls_seg_markers)} matchedcell{cell_diam} - Cell Segmentation Basins')>-1: + ls_scene.append(s_file.split('_')[0]) + d_match.update({s_file.split('_')[0]:s_file}) + elif s_file.find(f'{".".join(ls_seg_markers)} nuc{nuc_diam} matchedcell{cell_diam} - Cell Segmentation Basins')>-1: + ls_scene.append(s_file.split('_')[0]) + d_match.update({s_file.split('_')[0]:s_file}) + for s_scene in ['reg001']: #ls_scene: #one scene + print(f'processing {s_scene}') + for s_file in os.listdir(): + if s_file.find(s_scene) > -1: + if s_file.find("DAPI.png") > -1: + s_dapi = s_file + dapi = io.imread(s_dapi) + print(f'loading {s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif') + labels = io.imread(f'{s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif') + print(f'loading {d_match[s_scene]}') + cell_labels = io.imread(d_match[s_scene]) + #nuclear features + df_feat = features.extract_feat(labels,dapi, properties=(['mean_intensity'])) + df_feat.columns = [f'{item}_segmented-nuclei' for item in df_feat.columns] + df_feat.index = [f'{s_sample}_scene{s_scene.split("reg")[1]}_cell{item}' for item in df_feat.index] + + #get subcellular regions + cyto = features.label_difference(labels,cell_labels) + d_loc_nuc = features.subcellular_regions(labels, distance_short=2, distance_long=4) + d_loc_cell = features.subcellular_regions(cell_labels, distance_short=2, distance_long=4) + d_loc = {'nuclei':labels,'cell':cell_labels,'cytoplasm':cyto, + 'nucmem':d_loc_nuc['membrane'][0],'cellmem':d_loc_cell['membrane'][0], + 'perinuc4':d_loc_nuc['ring'][1],'exp4':d_loc_nuc['grown'][1], + 'nucadj2':d_loc_nuc['straddle'][0],'celladj2':d_loc_cell['straddle'][0]} + #subdir organized by slide or scene + if os.path.exists(f'{subdir}/{s_sample}'): + os.chdir(f'{subdir}/{s_sample}') + elif os.path.exists(f'{subdir}/{s_scene}'): + os.chdir(f'{subdir}/{s_scene}') + else: + os.chdir(f'{subdir}') + df_img = parse_img() + df_img['round_int'] = [int(re.sub('[^0-9]','', item)) for item in df_img.rounds] + df_img = df_img[df_img.round_int < 90] + df_img = df_img.sort_values('round_int') + df_scene = df_img# one scene [df_img.scene==s_scene.split("-Scene-")[1].split("_")[0]] + + #load each image + for s_index in df_scene.index: + intensity_image = io.imread(s_index) + df_thresh.loc[s_index,'threshold_li'] = filters.threshold_li(intensity_image) + if intensity_image.mean() > 0: + df_thresh.loc[s_index,'threshold_otsu'] = filters.threshold_otsu(intensity_image) + df_thresh.loc[s_index,'threshold_triangle'] = filters.threshold_triangle(intensity_image) + s_marker = df_scene.loc[s_index,'marker'] + print(f'extracting features {s_marker}') + #if s_marker == 'DAPI': + # s_marker = s_marker + f'{df_scene.loc[s_index,"rounds"].split("cyc")[1]}' + for s_loc, a_loc in d_loc.items(): + if s_loc == 'nuclei': + df_marker_loc = features.extract_feat(a_loc,intensity_image, properties=(['mean_intensity','centroid','area','eccentricity'])) + df_marker_loc.columns = [f'{s_marker}_{s_loc}',f'{s_marker}_{s_loc}_centroid-0',f'{s_marker}_{s_loc}_centroid-1',f'{s_marker}_{s_loc}_area',f'{s_marker}_{s_loc}_eccentricity'] + elif s_loc == 'cell': + df_marker_loc = features.extract_feat(a_loc,intensity_image, properties=(['mean_intensity','euler_number','area','eccentricity'])) + df_marker_loc.columns = [f'{s_marker}_{s_loc}',f'{s_marker}_{s_loc}_euler',f'{s_marker}_{s_loc}_area',f'{s_marker}_{s_loc}_eccentricity'] + else: + df_marker_loc = features.extract_feat(a_loc,intensity_image, properties=(['mean_intensity'])) + df_marker_loc.columns = [f'{s_marker}_{s_loc}'] + + #drop zero from array, set array ids as index + df_marker_loc.index = sorted(np.unique(a_loc)[1::]) + df_marker_loc.index = [f'{s_sample}_scene{s_scene.split("reg")[1]}_cell{item}' for item in df_marker_loc.index] + df_feat = df_feat.merge(df_marker_loc, left_index=True,right_index=True,how='left',suffixes=('',f'{s_marker}_{s_loc}')) + df_sample = df_sample.append(df_feat) + return(df_sample, df_thresh) + +def convert_tif(regdir,b_mkdir=True): + ''' + convert codex tif to standard tif + ''' + cwd = os.getcwd() + os.chdir(regdir) + for s_dir in sorted(os.listdir()): + if s_dir.find('reg')== 0: + os.chdir(s_dir) + for s_file in sorted(os.listdir()): + if s_file.find('.tif')>-1: + #s_round = s_file.split("Cycle(")[1].split(").ome.tif")[0] + #print(f'stain {s_round}') + #s_dir_new = s_dir.split('_')[2] + '-Scene-0' + s_dir.split('F-')[1] + #s_tissue_dir = s_dir.split('_F-')[0] + if b_mkdir: + preprocess.cmif_mkdir([f'{regdir}/converted_{s_dir}']) + a_dapi = skimage.io.imread(s_file) + with skimage.external.tifffile.TiffWriter(f'{regdir}/converted_{s_dir}/{s_file}') as tif: + tif.save(a_dapi) + os.chdir('..') + os.chdir(cwd) + +def visualize_reg_images(s_sample,regdir,qcdir,color='ch001'): + """ + array registered images to check tissue identity, focus, etc. + """ + #check registration + preprocess.cmif_mkdir([f'{qcdir}/RegisteredImages']) + cwd = os.getcwd() + os.chdir(regdir) + #for idx, s_dir in enumerate(sorted(os.listdir())): + # os.chdir(s_dir) + # s_sample = s_dir.split('-Scene')[0] + # print(s_sample) + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='reg',s_split='_') + df_img.rename({'data':'scene'},axis=1,inplace=True) + df_img['slide'] = s_sample + df_img['rounds'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[2] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[3].split('.')[0] for item in [item.split('_') for item in df_img.index]] + ls_scene = sorted(set(df_img.scene)) + for s_scene in ls_scene: + print(s_scene) + df_img_scene = df_img[df_img.scene == s_scene] + df_img_stain = df_img_scene[df_img_scene.color==color] + df_img_sort = df_img_stain.sort_values(['rounds']) + i_sqrt = math.ceil(math.sqrt(len(df_img_sort))) + fig = mpimage.array_img(df_img_sort,s_column='color',s_row='rounds',s_label='marker',tu_array=(i_sqrt,i_sqrt),tu_fig=(16,14)) + fig.savefig(f'{qcdir}/RegisteredImages/{s_scene}_registered_{color}.png') + os.chdir(cwd) + return(df_img_sort) + +def rename_files(d_rename,dir,b_test=True): + """ + change file names + """ + cwd = os.getcwd() + os.chdir(dir) + for idx, s_dir in enumerate(sorted(os.listdir())): + if s_dir.find('converted') == 0: + s_path = f'{dir}/{s_dir}' + os.chdir(s_path) + print(s_dir) + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='reg',s_split='_') + df_img.rename({'data':'scene'},axis=1,inplace=True) + df_img['rounds'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[2] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[3].split('.')[0] for item in [item.split('_') for item in df_img.index]] + if b_test: + print('This is a test') + preprocess.dchange_fname(d_rename,b_test=True) + elif b_test==False: + print('Changing name - not a test') + preprocess.dchange_fname(d_rename,b_test=False) + else: + pass + +def rename_fileorder(s_sample, dir, b_test=True): + """ + change file names + """ + cwd = os.getcwd() + os.chdir(dir) + for idx, s_dir in enumerate(sorted(os.listdir())): + if s_dir.find('converted') == 0: + s_path = f'{dir}/{s_dir}' + os.chdir(s_path) + print(s_dir) + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='Scene',s_split='_') + df_img.rename({'data':'scene'},axis=1,inplace=True) + df_img['rounds'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[2] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[3].split('.')[0] for item in [item.split('_') for item in df_img.index]] + for s_index in df_img.index: + s_round = df_img.loc[s_index,'rounds'] + s_scene= f"{s_sample}-{df_img.loc[s_index,'scene']}" + s_marker = df_img.loc[s_index,'marker'] + s_color = df_img.loc[s_index,'color'] + s_index_rename = f'{s_round}_{s_scene}_{s_marker}_{s_color}_ORG.tif' + d_rename = {s_index:s_index_rename} + if b_test: + print('This is a test') + preprocess.dchange_fname(d_rename,b_test=True) + elif b_test==False: + print('Changing name - not a test') + preprocess.dchange_fname(d_rename,b_test=False) + else: + pass + +def copy_files(dir,dapi_copy, marker_copy,testbool=True,type='codex'): + """ + copy and rename files if needed as dummies + need to edit + """ + os.chdir(dir) + for idx, s_dir in enumerate(sorted(os.listdir())): + if s_dir.find('converted') == 0: + s_path = f'{dir}/{s_dir}' + os.chdir(s_path) + #s_sample = s_dir.split('-Scene')[0] + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='R0',s_split='_') + df_img.rename({'data':'rounds'},axis=1,inplace=True) + df_img['scene'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[3] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[2].split('.')[0] for item in [item.split('_') for item in df_img.index]] + print(s_dir) + for key, dapi_item in dapi_copy.items(): + df_dapi = df_img[(df_img.rounds== key.split('_')[0]) & (df_img.color=='c1')] + s_dapi = df_dapi.loc[:,'marker'][0] + preprocess.copy_dapis(s_r_old=key,s_r_new=f'R{dapi_item}_',s_c_old='_c1_', + s_c_new='_c2_',s_find=f'_{s_dapi}_c1_ORG.tif',b_test=testbool,type=type) + i_count=0 + for idx,(key, item) in enumerate(marker_copy.items()): + preprocess.copy_markers(df_img, s_original=key, ls_copy = item, + i_last_round= dapi_item + i_count, b_test=testbool,type=type) + i_count=i_count + len(item) + return(df_img) + +def segmentation_thresholds(regdir,qcdir, d_segment): + """ + visualize binary mask of segmentaiton threholds + need to edit + """ + preprocess.cmif_mkdir([f'{qcdir}/Segmentation']) + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + if s_dir.find('converted') == 0: + s_path = f'{regdir}/{s_dir}' + os.chdir(s_path) + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='R',s_split='_') + df_img.rename({'data':'rounds'},axis=1,inplace=True) + df_img['scene'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[3] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[2].split('.')[0] for item in [item.split('_') for item in df_img.index]] + s_sample = s_dir + print(s_sample) + d_seg = preprocess.check_seg_markers(df_img,d_segment, i_rows=1, t_figsize=(6,6)) #few scenes + for key, fig in d_seg.items(): + fig.savefig(f'{qcdir}/Segmentation/{s_dir}_{key}_segmentation.png') + return(df_img) + +def parse_converted(dir): + ''' + parse codex filenames (coverted) + ''' + cwd = os.getcwd() + os.chdir(dir) + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='R',s_split='_') + df_img.rename({'data':'rounds'},axis=1,inplace=True) + df_img['scene'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[3] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[2] for item in [item.split('_') for item in df_img.index]] + os.chdir(cwd) + return(df_img) + +def segmentation_inputs(s_sample,regdir,segdir,d_segment,b_start=False): + """ + make inputs for guillaumes segmentation + """ + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + if s_dir.find('convert')== 0: + s_path = f'{regdir}/{s_dir}' + os.chdir(s_path) + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='R',s_split='_') + df_img.rename({'data':'rounds'},axis=1,inplace=True) + #df_img['rounds'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[3] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[2] for item in [item.split('_') for item in df_img.index]] + #s_sample = s_dir + #s_sample = s_dir.split('-Scene')[0] + print(s_sample) + df_marker = df_img[df_img.color!='c1'] + df_marker = df_marker.sort_values(['rounds','color']) + df_dapi = pd.DataFrame(index = [df_marker.marker.tolist()],columns=['rounds','colors','minimum','maximum','exposure','refexp','location']) + df_dapi['rounds'] = df_marker.loc[:,['rounds']].values + df_dapi['colors'] = df_marker.loc[:,['color']].values + df_dapi['minimum'] = 1003 + df_dapi['maximum'] = 65535 + df_dapi['exposure'] = 100 + df_dapi['refexp'] = 100 + df_dapi['location'] = 'All' + for s_key,i_item in d_segment.items(): + df_dapi.loc[s_key,'minimum'] = i_item + df_dapi.to_csv('RoundsCyclesTable.txt',sep=' ',header=False) + df_dapi.to_csv(f'metadata_{s_sample}_RoundsCyclesTable.csv',header=True) + #create cluster.java file + preprocess.cluster_java(s_dir=f'JE{idx}',s_sample=s_sample,imagedir=f'{s_path}',segmentdir=segdir,type='exacloud',b_segment=True,b_TMA=False) + if b_start: + os.chdir(f'/home/groups/graylab_share/Chin_Lab/ChinData/Work/engje/exacloud/JE{idx}') #exacloud + print(f'JE{idx}') + os.system('make_sh') + +def prepare_dataframe(s_sample,s_dapi,dapi_thresh,d_channel,ls_exclude,segdir,b_afsub=False): + ''' + filter data by last dapi, standard location, subtract AF, output treshold csv + ''' + + os.chdir(f'{segdir}') + #load data + df_mi = process.load_mi(s_sample) + df_xy = process.load_xy(s_sample) + #drop extra centroid columns,add scene column + df_xy = df_xy.loc[:,['DAPI_X','DAPI_Y']] + df_xy = process.add_scene(df_xy) + df_xy.to_csv(f'features_{s_sample}_CentroidXY.csv') + #filter by last DAPI + df_dapi_mi = process.filter_dapi(df_mi,df_xy,s_dapi,dapi_thresh,b_images=True) + df_t = process.load_meta(s_sample, s_path='./',type='LocationCsv') + #filter mean intensity by biomarker location in metadata + df_filter_mi = process.filter_loc(df_dapi_mi,df_t) + df_filter_mi.to_csv(f'features_{s_sample}_FilteredMeanIntensity_{s_dapi}{dapi_thresh}.csv') + if b_afsub: + #load metadata + df_t = pd.read_csv(f'metadata_{s_sample}_RoundsCyclesTableExposure.csv',index_col=0,header=0) + #normalize by exposure time, and save to csv + lb_columns = [len(set([item]).intersection(set(df_t.index)))>0 for item in [item.split('_')[0] for item in df_filter_mi.columns]] + df_filter_mi = df_filter_mi.loc[:,lb_columns] + df_norm = process.exposure_norm(df_filter_mi,df_t) + df_norm.to_csv(f'features_{s_sample}_ExpNormalizedMeanIntensity_{s_dapi}{dapi_thresh}.csv') + #subtract AF channels in data + df_sub,ls_sub,ls_record = process.af_subtract(df_norm,df_t,d_channel,ls_exclude) + df_out = process.output_subtract(df_sub,df_t) + df_out.to_csv(f'features_{s_sample}_AFSubtractedMeanIntensity_{s_dapi}{dapi_thresh}.csv') + f = open(f"{s_sample}_AFsubtractionData.txt", "w") + f.writelines(ls_record) + f.close() + else: + df_out = df_filter_mi + #output thresholding csv + df_out = process.add_scene(df_out) #df_out + df_thresh = process.make_thresh_df(df_out,ls_drop=None) + df_thresh.to_csv(f'thresh_XX_{s_sample}.csv') + +def multipage_tiff(d_combos,s_dapi,regdir): + ''' + make custom overlays, either original of AF subtracted, save at 8 bit for size, and thresholding + ''' + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + if s_dir.find('convert')== 0: + s_path = f'{regdir}/{s_dir}' + os.chdir(s_path) + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='R',s_split='_') + df_img.rename({'data':'rounds'},axis=1,inplace=True) + df_img['color'] = [item[3] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[2] for item in [item.split('_') for item in df_img.index]] + df_img['scene'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['imagetype'] = [item[4].split('.')[0] for item in [item.split('_') for item in df_img.index]] + df_dapi = df_img[df_img.marker.str.contains(s_dapi.split('_')[0])] + df_img_stain = df_img[(~df_img.marker.str.contains('DAPI'))] + #check + es_test = set() + for key, item in d_combos.items(): + es_test = es_test.union(item) + print(set(df_img_stain.marker) - es_test) + process.custom_overlays(d_combos, df_img_stain, df_dapi) + else: + continue + +def load_crop_labels(d_crop,tu_dim,segdir,cropdir,s_find='Nuclei Segmentation Basins'): + """ + crop the segmentation basins (cell of nuceli) to same coord as images for veiwing in Napari + s_find: 'exp5_CellSegmentationBasins' or 'Nuclei Segmentation Basins' + """ + cwd = os.getcwd() + for s_scene, xy_cropcoor in d_crop.items(): + print(s_scene) + s_sample = s_scene.split('-Scene-')[0] + os.chdir(f'{segdir}') + + for s_file in os.listdir(): + if s_file.find(s_find) > -1: #Nuclei Segmentation Basins.tif #Cell Segmentation Basins.tif + if s_file.find(s_scene.split(s_sample)[1]) > -1: + a_seg = skimage.io.imread(s_file) + a_crop = a_seg[(xy_cropcoor[1]):(xy_cropcoor[1]+tu_dim[1]),(xy_cropcoor[0]):(xy_cropcoor[0]+tu_dim[0])] + s_coor = f'x{xy_cropcoor[0]}y{xy_cropcoor[1]}.tif' + #crop file + s_file_new = f'{cropdir}/{s_sample}_{s_file.replace(" ","").replace(".tif",s_coor)}' + print(s_file_new) + skimage.io.imsave(s_file_new,a_crop) + os.chdir(cwd) diff --git a/mplex_image/features.py b/mplex_image/features.py new file mode 100755 index 0000000..7812462 --- /dev/null +++ b/mplex_image/features.py @@ -0,0 +1,603 @@ +#### +# title: features.py +# language: Python3.7 +# date: 2020-06-00 +# license: GPL>=v3 +# author: Jenny +# description: +# python3 script for single cell feature extraction +#### + +#libraries +import os +import sys +import numpy as np +import pandas as pd +import shutil +import skimage +import scipy +from scipy import stats +from scipy import ndimage as ndi +from skimage import measure, segmentation, morphology +from skimage import io, filters +import re +import json +from biotransistor import imagine +from PIL import Image +from mplex_image import process +import matplotlib.pyplot as plt +Image.MAX_IMAGE_PIXELS = 1000000000 + +#functions +def extract_feat(labels,intensity_image, properties=('centroid','mean_intensity','area','eccentricity')): + ''' + given labels and intensity image, extract features to dataframe + ''' + props = measure.regionprops_table(labels,intensity_image, properties=properties) + df_prop = pd.DataFrame(props) + return(df_prop) + +def expand_label(labels,distance=3): + ''' + expand the nucelar labels by a fixed number of pixels + ''' + boundaries = segmentation.find_boundaries(labels,mode='outer') #thick + shrunk_labels = labels.copy() + shrunk_labels[boundaries] = 0 + background = shrunk_labels == 0 + distances, (i, j) = scipy.ndimage.distance_transform_edt( + background, return_indices=True + ) + + grown_labels = labels.copy() + mask = background & (distances <= distance) + grown_labels[mask] = shrunk_labels[i[mask], j[mask]] + ring_labels = grown_labels - shrunk_labels + + return(ring_labels, grown_labels) #shrunk_labels, grown_labels, + +def contract_label(labels,distance=3): + ''' + contract labels by a fixed number of pixels + ''' + boundaries = segmentation.find_boundaries(labels,mode='outer') + shrunk_labels = labels.copy() + shrunk_labels[boundaries] = 0 + foreground = shrunk_labels != 0 + distances, (i, j) = scipy.ndimage.distance_transform_edt( + foreground, return_indices=True + ) + + mask = foreground & (distances <= distance) + shrunk_labels[mask] = shrunk_labels[i[mask], j[mask]] + rim_labels = labels - shrunk_labels + return(rim_labels) + +def straddle_label(labels,distance=3): + ''' + expand and contract labels by a fixed number of pixels + ''' + boundaries = segmentation.find_boundaries(labels,mode='outer') #outer + shrunk_labels = labels.copy() + grown_labels = labels.copy() + shrunk_labels[boundaries] = 0 + foreground = shrunk_labels != 0 + background = shrunk_labels == 0 + distances_f, (i, j) = scipy.ndimage.distance_transform_edt( + foreground, return_indices=True + ) + distances_b, (i, j) = scipy.ndimage.distance_transform_edt( + background, return_indices=True + ) + mask_f = foreground & (distances_f <= distance) + mask_b = background & (distances_b <= distance + 1) + shrunk_labels[mask_f] = 0 + grown_labels[mask_b] = grown_labels[i[mask_b], j[mask_b]] + membrane_labels = grown_labels - shrunk_labels + return(membrane_labels, grown_labels, shrunk_labels) + +def label_difference(labels,cell_labels): + ''' + given matched nuclear and cell label IDs,return cell_labels minus labels + ''' + overlap = cell_labels==labels + ring_rep = cell_labels.copy() + ring_rep[overlap] = 0 + return(ring_rep) + +def get_mip(ls_img): + ''' + maximum intensity projection of images (input list of filenames) + ''' + imgs = [] + for s_img in ls_img: + img = io.imread(s_img) + imgs.append(img) + mip = np.stack(imgs).max(axis=0) + return(mip) + +def thresh_li(img,area_threshold=100,low_thresh=1000): + ''' + threshold an image with Li’s iterative Minimum Cross Entropy method + if too low, apply the low threshold instead (in case negative) + ''' + mask = img >= filters.threshold_li(img) + mask = morphology.remove_small_holes(mask, area_threshold=area_threshold) + mask[mask < low_thresh] = 0 + return(mask) + +def mask_border(mask,type='inner',pixel_distance = 50): + ''' + for inner, distance transform from mask to background + for outer, distance transform from back ground to mask + returns a mask + ''' + shrunk_mask = mask.copy() + if type == 'inner': + foreground = ~mask + background = mask + elif type == 'outer': + foreground = ~mask + background = mask + distances, (i, j) = scipy.ndimage.distance_transform_edt( + background, return_indices=True + ) + maskdist = mask & (distances <= pixel_distance) + shrunk_mask[maskdist] = shrunk_mask[i[maskdist], j[maskdist]] + mask_out = np.logical_and(mask,np.logical_not(shrunk_mask)) + return(mask_out,shrunk_mask,maskdist,distances) + +def mask_labels(mask,labels): + '''' + return the labels that fall within the mask + ''' + selected_array = labels[mask] + a_unique = np.unique(selected_array) + return(a_unique) + +def parse_org(s_end = "ORG.tif",s_start='R'): + """ + This function will parse images following koei's naming convention + Example: Registered-R1_PCNA.CD8.PD1.CK19_Her2B-K157-Scene-002_c1_ORG.tif + The output is a dataframe with image filename in index + And rounds, color, imagetype, scene (/tissue), and marker in the columns + """ + ls_file = [] + for file in os.listdir(): + if file.endswith(s_end): + if file.find(s_start)==0: + ls_file = ls_file + [file] + df_img = pd.DataFrame(index=ls_file) + df_img['rounds'] = [item.split('_')[0].split('Registered-')[1] for item in df_img.index] + df_img['color'] = [item.split('_')[-2] for item in df_img.index] + df_img['slide'] = [item.split('_')[2] for item in df_img.index] + df_img['scene'] = [item.split('-Scene-')[1] for item in df_img.slide] + #parse file name for biomarker + for s_index in df_img.index: + #print(s_index) + s_color = df_img.loc[s_index,'color'] + if s_color == 'c1': + s_marker = 'DAPI' + elif s_color == 'c2': + s_marker = s_index.split('_')[1].split('.')[0] + elif s_color == 'c3': + s_marker = s_index.split('_')[1].split('.')[1] + elif s_color == 'c4': + s_marker = s_index.split('_')[1].split('.')[2] + elif s_color == 'c5': + s_marker = s_index.split('_')[1].split('.')[3] + elif s_color == 'c6': + s_marker = s_index.split('_')[1].split('.')[2] + elif s_color == 'c7': + s_marker = s_index.split('_')[1].split('.')[3] + else: print('Error') + df_img.loc[s_index,'marker'] = s_marker + return(df_img) + +def extract_cellpose_features(s_sample, segdir, subdir, ls_seg_markers, nuc_diam, cell_diam,b_big=False): #,b_thresh=False + ''' + load the segmentation results, the input images, and the channels images + extract mean intensity from each image, and centroid, area and eccentricity for + ''' + + df_sample = pd.DataFrame() + df_thresh = pd.DataFrame() + + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation') + ls_scene = [] + d_match = {} + for s_file in os.listdir(): + if s_file.find(f'{".".join(ls_seg_markers)} matchedcell{cell_diam} - Cell Segmentation Basins')>-1: + ls_scene.append(s_file.split('_')[0]) + d_match.update({s_file.split('_')[0]:s_file}) + elif s_file.find(f'{".".join(ls_seg_markers)} nuc{nuc_diam} matchedcell{cell_diam} - Cell Segmentation Basins')>-1: + ls_scene.append(s_file.split('_')[0]) + d_match.update({s_file.split('_')[0]:s_file}) + for s_scene in ls_scene: + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation') + print(f'processing {s_scene}') + for s_file in os.listdir(): + if s_file.find(s_scene) > -1: + if s_file.find("DAPI.png") > -1: + s_dapi = s_file + dapi = io.imread(f'{segdir}/{s_sample}Cellpose_Segmentation/{s_dapi}') + print(f'loading {s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif') + labels = io.imread(f'{s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif') + cell_labels = io.imread(f'{segdir}/{s_sample}Cellpose_Segmentation/{d_match[s_scene]}') + print(f'loading {d_match[s_scene]}') + #nuclear features + df_feat = extract_feat(labels,dapi, properties=(['label'])) + df_feat.columns = [f'{item}_segmented-nuclei' for item in df_feat.columns] + df_feat.index = [f'{s_sample}_scene{s_scene.split("-Scene-")[1].split("_")[0]}_cell{item}' for item in df_feat.loc[:,'label_segmented-nuclei']] + + #get subcellular regions + cyto = label_difference(labels,cell_labels) + d_loc_nuc = subcellular_regions(labels, distance_short=2, distance_long=5) + d_loc_cell = subcellular_regions(cell_labels, distance_short=2, distance_long=5) + d_loc = {'nuclei':labels,'cell':cell_labels,'cytoplasm':cyto, + 'nucmem':d_loc_nuc['membrane'][0],'cellmem':d_loc_cell['membrane'][0], + 'perinuc5':d_loc_nuc['ring'][1],'exp5':d_loc_nuc['grown'][1], + 'nucadj2':d_loc_nuc['straddle'][0],'celladj2':d_loc_cell['straddle'][0]} + + #subdir organized by slide or scene + if os.path.exists(f'{subdir}/{s_sample}'): + os.chdir(f'{subdir}/{s_sample}') + elif os.path.exists(f'{subdir}/{s_scene}'): + os.chdir(f'{subdir}/{s_scene}') + else: + os.chdir(f'{subdir}') + df_img = parse_org() + df_img['round_int'] = [int(re.sub('[^0-9]','', item)) for item in df_img.rounds] + df_img = df_img[df_img.round_int < 90] + df_img = df_img.sort_values('round_int') + df_scene = df_img[df_img.scene==s_scene.split("-Scene-")[1].split("_")[0]] + + #load each image + for s_index in df_scene.index: + intensity_image = io.imread(s_index) + df_thresh.loc[s_index,'threshold_li'] = filters.threshold_li(intensity_image) + if intensity_image.mean() > 0: + df_thresh.loc[s_index,'threshold_otsu'] = filters.threshold_otsu(intensity_image) + df_thresh.loc[s_index,'threshold_triangle'] = filters.threshold_triangle(intensity_image) + #if b_thresh: + # break + s_marker = df_scene.loc[s_index,'marker'] + print(f'extracting features {s_marker}') + if s_marker == 'DAPI': + s_marker = s_marker + f'{df_scene.loc[s_index,"rounds"].split("R")[1]}' + for s_loc, a_loc in d_loc.items(): + if s_loc == 'nuclei': + df_marker_loc = extract_feat(a_loc,intensity_image, properties=(['mean_intensity','centroid','area','eccentricity','label'])) + df_marker_loc.columns = [f'{s_marker}_{s_loc}',f'{s_marker}_{s_loc}_centroid-0',f'{s_marker}_{s_loc}_centroid-1',f'{s_marker}_{s_loc}_area',f'{s_marker}_{s_loc}_eccentricity',f'{s_marker}_{s_loc}_label'] + elif s_loc == 'cell': + df_marker_loc = extract_feat(a_loc,intensity_image, properties=(['mean_intensity','euler_number','area','eccentricity','label'])) + df_marker_loc.columns = [f'{s_marker}_{s_loc}',f'{s_marker}_{s_loc}_euler',f'{s_marker}_{s_loc}_area',f'{s_marker}_{s_loc}_eccentricity',f'{s_marker}_{s_loc}_label'] + else: + df_marker_loc = extract_feat(a_loc,intensity_image, properties=(['mean_intensity','label'])) + df_marker_loc.columns = [f'{s_marker}_{s_loc}',f'{s_marker}_{s_loc}_label'] + #drop zero from array, set array ids as index + #old df_marker_loc.index = sorted(np.unique(a_loc)[1::]) + df_marker_loc.index = df_marker_loc.loc[:,f'{s_marker}_{s_loc}_label'] + df_marker_loc.index = [f'{s_sample}_scene{s_scene.split("-Scene-")[1].split("_")[0]}_cell{item}' for item in df_marker_loc.index] + df_feat = df_feat.merge(df_marker_loc, left_index=True,right_index=True,how='left',suffixes=('',f'{s_marker}_{s_loc}')) + if b_big: + df_feat.to_csv(f'{segdir}/{s_sample}Cellpose_Segmentation/features_{s_sample}-{s_scene}.csv') + df_sample = df_sample.append(df_feat) + return(df_sample, df_thresh) + +def extract_bright_features(s_sample, segdir, subdir, ls_seg_markers, nuc_diam, cell_diam,ls_membrane): + ''' + load the features, segmentation results, the input images, and the channels images + extract mean intensity of the top 25% of pixel in from each label region + ''' + df_sample = pd.DataFrame() + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation') + ls_scene = [] + d_match = {} + for s_file in os.listdir(): + if s_file.find(f'{".".join(ls_seg_markers)} matchedcell{cell_diam} - Cell Segmentation Basins')>-1: + ls_scene.append(s_file.split('_')[0]) + d_match.update({s_file.split('_')[0]:s_file}) + elif s_file.find(f'{".".join(ls_seg_markers)} nuc{nuc_diam} matchedcell{cell_diam} - Cell Segmentation Basins')>-1: + ls_scene.append(s_file.split('_')[0]) + d_match.update({s_file.split('_')[0]:s_file}) + for s_scene in ls_scene: + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation') + print(f'processing {s_scene}') + for s_file in os.listdir(): + if s_file.find(s_scene) > -1: + if s_file.find("DAPI.png") > -1: + s_dapi = s_file + dapi = io.imread(f'{segdir}/{s_sample}Cellpose_Segmentation/{s_dapi}') + print(f'loading {s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif') + labels = io.imread(f'{s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif') + print(labels.shape) + cell_labels = io.imread(f'{segdir}/{s_sample}Cellpose_Segmentation/{d_match[s_scene]}') + print(cell_labels.shape) + print(f'loading {d_match[s_scene]}') + #nuclear features + df_feat = extract_feat(labels,dapi, properties=(['label'])) + df_feat.columns = [f'{item}_segmented-nuclei' for item in df_feat.columns] + df_feat.index = [f'{s_sample}_scene{s_scene.split("-Scene-")[1].split("_")[0]}_cell{item}' for item in df_feat.loc[:,'label_segmented-nuclei']] + + #get subcellular regions + d_loc_nuc = subcellular_regions(labels, distance_short=2, distance_long=5) + d_loc_cell = subcellular_regions(cell_labels, distance_short=2, distance_long=5) + d_loc = {'nucmem25':d_loc_nuc['membrane'][0],'exp5nucmembrane25':d_loc_nuc['grown'][1], + 'cellmem25':d_loc_cell['membrane'][0],'nuclei25':labels} + + #subdir organized by slide or scene + if os.path.exists(f'{subdir}/{s_sample}'): + os.chdir(f'{subdir}/{s_sample}') + elif os.path.exists(f'{subdir}/{s_scene}'): + os.chdir(f'{subdir}/{s_scene}') + else: + os.chdir(f'{subdir}') + df_img = parse_org() + df_img['round_int'] = [int(re.sub('[^0-9]','', item)) for item in df_img.rounds] + df_img = df_img[df_img.round_int < 90] + df_img = df_img.sort_values('round_int') + df_scene = df_img[df_img.scene==s_scene.split("-Scene-")[1].split("_")[0]] + df_marker = df_scene[df_scene.marker.isin(ls_membrane)] + #load each image + for s_index in df_marker.index: + print(f'loading {s_index}') + intensity_image = io.imread(s_index) + #print(intensity_image.shape) + s_marker = df_marker.loc[s_index,'marker'] + print(f'extracting features {s_marker}') + if s_marker == 'DAPI': + s_marker = s_marker + f'{df_marker.loc[s_index,"rounds"].split("R")[1]}' + for s_loc, a_loc in d_loc.items(): + #print(a_loc.shape) + df_marker_loc = pd.DataFrame(columns = [f'{s_marker}_{s_loc}']) + df_prop = extract_feat(a_loc,intensity_image, properties=(['intensity_image','image','label'])) + for idx in df_prop.index: + label_id = df_prop.loc[idx,'label'] + intensity_image_small = df_prop.loc[idx,'intensity_image'] + image = df_prop.loc[idx,'image'] + pixels = intensity_image_small[image] + pixels25 = pixels[pixels >= np.quantile(pixels,.75)] + df_marker_loc.loc[label_id,f'{s_marker}_{s_loc}'] = pixels25.mean() + df_marker_loc.index = [f'{s_sample}_scene{s_scene.split("-Scene-")[1].split("_")[0]}_cell{item}' for item in df_marker_loc.index] + df_feat = df_feat.merge(df_marker_loc, left_index=True,right_index=True,how='left',suffixes=('',f'{s_marker}_{s_loc}')) + df_sample = df_sample.append(df_feat) + #break + return(df_sample) + +def subcellular_regions(labels, distance_short=2, distance_long=5): + ''' + calculate subcellular segmentation regions from segmentation mask + ''' + membrane_short = contract_label(labels,distance=distance_short) + membrane_long = contract_label(labels,distance=distance_long) + ring_short, grown_short = expand_label(labels,distance=distance_short) + ring_long, grown_long = expand_label(labels,distance=distance_long) + straddle_short, __, shrink_short = straddle_label(labels,distance=distance_short) + straddle_long, __, shrink_long = straddle_label(labels,distance=distance_long) + d_loc_sl={'membrane':(membrane_short,membrane_long), + 'ring':(ring_short,ring_long), + 'straddle':(straddle_short,straddle_long), + 'grown':(grown_short,grown_long), + 'shrunk':(shrink_short,shrink_long)} + return(d_loc_sl) + +def combine_labels(s_sample,segdir, subdir, ls_seg_markers, nuc_diam, cell_diam, df_mi_full,s_thresh): + ''' + - load cell labels; delete cells that were not used for cytoplasm (i.e. ecad neg) + - nuc labels, expand to perinuc 5 and then cut out the cell labels + - keep track of cells that are completely coverd by another cell (or two or three: counts as touching). + ''' + se_neg = df_mi_full[df_mi_full.slide == s_sample].loc[:,f'{s_thresh}_negative'] + print(len(se_neg)) + dd_result = {} + if os.path.exists(f'{segdir}/{s_sample}Cellpose_Segmentation'): + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation') + else: + os.chdir(segdir) + print(segdir) + ls_scene = [] + for s_file in os.listdir(): + if s_file.find(' - DAPI.png') > -1: + ls_scene.append(s_file.split(' - DAPI.png')[0]) + ls_scene_all = sorted(set([item.split('_cell')[0].replace('_scene','-Scene-') for item in se_neg.index]) & set(ls_scene)) + if len(ls_scene_all) == 0: + ls_scene_all = sorted(set([item.split('_cell')[0].replace('_scene','-Scene-').split('_')[1] for item in se_neg.index]) & set(ls_scene)) + print(ls_scene_all) + for s_scene in ls_scene_all: + se_neg_scene = se_neg[se_neg.index.str.contains(s_scene.replace("Scene ","scene")) | se_neg.index.str.contains(s_scene.replace("-Scene-","_scene"))] + print(f'Processing combined segmentaiton labels for {s_scene}') + if os.path.exists(f'{s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif'): + labels = io.imread(f'{s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif') + else: + print('no nuclei labels found') + if os.path.exists(f'{s_scene} matchedcell{cell_diam} - Cell Segmentation Basins.tif'): + cell_labels = io.imread(f'{s_scene} matchedcell{cell_diam} - Cell Segmentation Basins.tif') + elif os.path.exists(f'{s_scene}_{".".join(ls_seg_markers)} matchedcell{cell_diam} - Cell Segmentation Basins.tif'): + cell_labels = io.imread(f'{s_scene}_{".".join(ls_seg_markers)} matchedcell{cell_diam} - Cell Segmentation Basins.tif') + elif os.path.exists(f'{s_scene}_{".".join(ls_seg_markers)} nuc{nuc_diam} matchedcell{cell_diam} - Cell Segmentation Basins.tif'): + cell_labels = io.imread(f'{s_scene}_{".".join(ls_seg_markers)} nuc{nuc_diam} matchedcell{cell_diam} - Cell Segmentation Basins.tif') + else: + print('no cell labels found') + #set non-ecad cell labels to zero + a_zeros = np.array([int(item.split('_cell')[1]) for item in se_neg_scene[se_neg_scene].index]).astype('int64') + mask = np.isin(cell_labels, a_zeros) + cell_labels_copy = cell_labels.copy() + cell_labels_copy[mask] = 0 + #make the nuclei under cells zero + labels_copy = labels.copy() + distance = 5 + perinuc5, labels_exp = expand_label(labels,distance=distance) + labels_exp[cell_labels_copy > 0] = 0 + #combine calls and expanded nuclei + combine = (labels_exp + cell_labels_copy) + if s_scene.find('Scene') == 0: + io.imsave(f'{s_sample}_{s_scene.replace("Scene ","scene")}_cell{cell_diam}_nuc{nuc_diam}_CombinedSegmentationBasins.tif',combine) + else: + io.imsave(f'{s_scene}_{".".join(ls_seg_markers)}-cell{cell_diam}_exp{distance}_CellSegmentationBasins.tif',combine) + #figure out the covered cells...labels + combined + not_zero_pixels = np.array([labels.ravel() !=0,combine.ravel() !=0]).all(axis=0) + a_tups = np.array([combine.ravel()[not_zero_pixels],labels.ravel()[not_zero_pixels]]).T #combined over nuclei + unique_rows = np.unique(a_tups, axis=0) + new_dict = {} + for key, value in unique_rows: + if key == value: + continue + else: + if key in new_dict: + new_dict[key].append(value) + else: + new_dict[key] = [value] + #from elmar (reformat cells touching dictionary and save + d_result = {} + for i_cell, li_touch in new_dict.items(): + d_result.update({str(i_cell): [str(i_touch) for i_touch in li_touch]}) + dd_result.update({f'{s_sample}_{s_scene.replace("Scene ","scene")}':d_result}) + #save dd_touch as json file + with open(f'result_{s_sample}_cellsatop_dictionary.json','w') as f: + json.dump(dd_result, f) + print('') + return(labels,combine,dd_result) + +def check_basins(cell_labels, cell_diam): + dai_value = {'a':cell_labels} + df = imagine.membrane_px(cell_labels,dai_value) + ls_bad = sorted(set(df[df.x_relative > 10*cell_diam].cell) | set(df[df.y_relative > 10*cell_diam].cell)) + return(ls_bad) + +def check_combined(segdir,s_sample,cell_diam,ls_seg_markers): + df_result = pd.DataFrame() + if os.path.exists(f'{segdir}/{s_sample}Cellpose_Segmentation'): + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation') + else: + os.chdir(segdir) + ls_scene = [] + for s_file in os.listdir(): + if s_file.find(' - DAPI.png') > -1: + ls_scene.append(s_file.split(' - DAPI.png')[0]) + for s_scene in sorted(ls_scene): + print(s_scene) + if os.path.exists(f'{s_scene}_{".".join(ls_seg_markers)}-cell{cell_diam}_exp5_CellSegmentationBasins.tif'): + cell_labels = io.imread(f'{s_scene}_{".".join(ls_seg_markers)}-cell{cell_diam}_exp5_CellSegmentationBasins.tif') + print(f'Loaded {s_scene}_{".".join(ls_seg_markers)}-cell{cell_diam}_exp5_CellSegmentationBasins.tif') + ls_bad = check_basins(cell_labels, cell_diam) + ls_bad_cells = [f"{s_scene.replace('-Scene-','_scene')}_cell{item}" for item in ls_bad] + df_bad = pd.DataFrame(index=ls_bad_cells,columns=['bad_match'],data=[True]*len(ls_bad_cells)) + df_result = df_result.append(df_bad) + else: + print('no combined cell labels found') + return(df_result) + +def edge_mask(s_sample,segdir,subdir,i_pixel=154, dapi_thresh=350,i_fill=50000,i_close=20): + ''' + find edge of the tissue. first, find tissue by threshodling DAPI R1 (pixels above dapi_thresh) + then, mask all pixels within i_pixel distance of tissue border + return/save binary mask + ''' + os.chdir(segdir) + df_img = process.load_li([s_sample],s_thresh='', man_thresh=100) + for s_scene in sorted(set(df_img.scene)): + print(f'Calculating tissue edge mask for Scene {s_scene}') + s_index = df_img[(df_img.scene == s_scene) & (df_img.rounds == 'R1') & (df_img.color =='c1')].index[0] + if os.path.exists(f'{subdir}/{s_sample}/{s_index}'): + img_dapi = io.imread(f'{subdir}/{s_sample}/{s_index}') + elif os.path.exists(f'{subdir}/{s_sample}-Scene-{s_scene}/{s_index}'): + img_dapi = io.imread(f'{subdir}/{s_sample}-Scene-{s_scene}/{s_index}') + else: + print('no DAPI found') + img_dapi = np.zeros([2,2]) + mask = img_dapi > dapi_thresh + mask_small = morphology.remove_small_objects(mask, min_size=100) + mask_closed = morphology.binary_closing(mask_small, morphology.octagon(i_close,i_close//2)) + mask_filled = morphology.remove_small_holes(mask_closed, i_fill) + border_mask, __, __,distances = mask_border(mask_filled,type='inner',pixel_distance = i_pixel) + img = np.zeros(border_mask.shape,dtype='uint8') + img[border_mask] = 255 + io.imsave(f"{segdir}/TissueEdgeMask{i_pixel}_{s_sample}_scene{s_scene}.png", img) + +def edge_hull(s_sample,segdir,subdir,i_pixel=154, dapi_thresh=350,i_fill=50000,i_close=40,i_small=30000): + ''' + find edge of the tissue. first, find tissue by threshodling DAPI R1 (pixels above dapi_thresh) + then, mask all pixels within i_pixel distance of tissue border + return/save binary mask + ''' + os.chdir(segdir) + df_img = process.load_li([s_sample],s_thresh='', man_thresh=100) + for s_scene in sorted(set(df_img.scene)): + print(f'Calculating tissue edge mask for Scene {s_scene}') + s_index = df_img[(df_img.scene == s_scene) & (df_img.rounds == 'R1') & (df_img.color =='c1')].index[0] + if os.path.exists(f'{subdir}/{s_sample}/{s_index}'): + img_dapi = io.imread(f'{subdir}/{s_sample}/{s_index}') + elif os.path.exists(f'{subdir}/{s_sample}-Scene-{s_scene}/{s_index}'): + img_dapi = io.imread(f'{subdir}/{s_sample}-Scene-{s_scene}/{s_index}') + else: + print('no DAPI found') + img_dapi = np.zeros([2,2]) + mask = img_dapi > dapi_thresh + mask_small = morphology.remove_small_objects(mask, min_size=100) + mask_closed = morphology.binary_closing(mask_small, morphology.octagon(i_close,i_close//2)) + mask_filled = morphology.remove_small_holes(mask_closed, i_fill) + mask_smaller = morphology.remove_small_objects(mask, min_size=i_small) + mask_hull = morphology.convex_hull_image(mask_smaller) + border_mask, __, __,distances = mask_border(mask_filled,type='inner',pixel_distance = i_pixel) + img = np.zeros(border_mask.shape,dtype='uint8') + img[border_mask] = 255 + io.imsave(f"{segdir}/TissueEdgeMask{i_pixel}_{s_sample}_scene{s_scene}.png", img) + +def edge_cells(s_sample,segdir,nuc_diam,i_pixel=154): + ''' + load a binary mask of tissue, cell labels, and xy coord datafreame. + return data frame of cells witin binary mask + ''' + df_sample = pd.DataFrame() + #load xy + df_xy = pd.read_csv(f'{segdir}/features_{s_sample}_CentroidXY.csv',index_col=0) + df_xy['cells'] = [int(item.split('cell')[1]) for item in df_xy.index] + ls_scene = sorted(set([item.split('_')[1].split('scene')[1] for item in df_xy.index])) + #load masks + for s_scene in ls_scene: + print(f'Calculating edge cells for Scene {s_scene}') + mask = io.imread(f"{segdir}/TissueEdgeMask{i_pixel}_{s_sample}_scene{s_scene}.png") + mask_gray = mask == 255 + labels = io.imread(f'{segdir}/{s_sample}Cellpose_Segmentation/{s_sample}-Scene-{s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif') + edge = mask_labels(mask_gray,labels) + df_scene = df_xy[df_xy.index.str.contains(f'{s_sample}_scene{s_scene}')] + #works + es_cells = set(edge.astype('int')).intersection(set(df_scene.cells)) + df_edge = df_scene[df_scene.cells.isin(es_cells)] + fig,ax=plt.subplots() + ax.imshow(mask_gray) + ax.scatter(df_edge.DAPI_X,df_edge.DAPI_Y,s=1) + fig.savefig(f'{segdir}/TissueEdgeMask{i_pixel}_{s_sample}-Scene-{s_scene}_cells.png') + df_sample = df_sample.append(df_edge) + return(df_sample) + +def cell_distances(df_xy,s_scene,distances): + ''' + load a binary mask of tissue, cell labels, and xy coord datafreame. + return data frame of cells witin binary mask + ''' + df_xy['DAPI_Y'] = df_xy.DAPI_Y.astype('int64') + df_xy['DAPI_X'] = df_xy.DAPI_X.astype('int64') + print(f'Calculating distances for Scene {s_scene}') + df_scene = df_xy[df_xy.index.str.contains(f"{s_scene.replace('-Scene-','_scene')}")].copy() + df_scene['pixel_dist'] = distances[df_scene.DAPI_Y,df_scene.DAPI_X] + return(df_scene) + +def cell_coords(): + ''' + TBD: find cell coordinate within a mask + ''' + for s_scene in ls_scene: + #old (use if you have coordinates, not labels) + #mask_gray = mask#[:,:,0] + #contour = skimage.measure.find_contours(mask_gray,0) + #coords = skimage.measure.approximate_polygon(contour[0], tolerance=5) + #fig,ax=plt.subplots() + #ax.imshow(mask_gray) + #ax.plot(coords[:, 1], coords[:, 0], '-r', linewidth=2) + #fig.savefig(f'TissueEdgeMask_{s_sample}_Scene-{s_scene}_polygon.png') + #x = np.array(df_scene.DAPI_X.astype('int').values) + #y = np.array(df_scene.DAPI_Y.astype('int').values) + #points = np.array((y,x)).T + mask = skimage.measure.points_in_poly(points, coords) \ No newline at end of file diff --git a/mplex_image/gating.py b/mplex_image/gating.py new file mode 100755 index 0000000..a3665fc --- /dev/null +++ b/mplex_image/gating.py @@ -0,0 +1,205 @@ +##### +# gating.py +# author: engje, grael +# date: 2020-04-07 +# license: GPLv3 +##### + +# library +import os +import pandas as pd +import shutil +from mplex_image import analyze +import numpy as np + + +def main_celltypes(df_data,ls_endothelial,ls_immune,ls_tumor,ls_cellline_index): + #celltpye + #1 endothelial + df_data['endothelial'] = df_data.loc[:,ls_endothelial].any(axis=1) + #2 immune + ls_exclude = ls_endothelial + df_data['immune'] = df_data.loc[:,ls_immune].any(axis=1) & ~df_data.loc[:,ls_exclude].any(axis=1) + #3 tumor + ls_exclude = ls_endothelial + ls_immune + df_data['tumor'] = df_data.loc[:,ls_tumor].any(axis=1) & ~df_data.loc[:,ls_exclude].any(axis=1) + #4 stromal + ls_exclude = ls_immune + ls_endothelial + ls_tumor + df_data['stromal'] = ~df_data.loc[:,ls_exclude].any(axis=1) + #add celltype + ls_cell_names = ['stromal','endothelial','tumor','immune'] + s_type_name = 'celltype' + analyze.add_celltype(df_data, ls_cell_names, s_type_name) + #fix cell lines (all tumor!) + df_data['slide_scene'] = [item.split('_cell')[0] for item in df_data.index] + df_data.loc[df_data[df_data.slide_scene.isin(ls_cellline_index)].index,'celltype'] = 'tumor' + df_data['immune'] = df_data.loc[:,'celltype'] == 'immune' + df_data['stromal'] = df_data.loc[:,'celltype'] == 'stromal' + df_data['endothelial'] = df_data.loc[:,'celltype'] == 'endothelial' + return(df_data) + +def proliferation(df_data,ls_prolif): + #proliferation + df_data['prolif'] = df_data.loc[:,ls_prolif].any(axis=1) + df_data['nonprolif'] = ~df_data.loc[:,ls_prolif].any(axis=1) + #add proliferation + ls_cell_names = ['prolif','nonprolif'] + s_type_name = 'proliferation' + analyze.add_celltype(df_data, ls_cell_names, s_type_name) + return(df_data) + +def immune_types(df_data,s_myeloid,s_bcell,s_tcell): + ## T cell, B cell or myeloid + df_data['CD68Mac'] = df_data.loc[:,[s_myeloid,'immune']].all(axis=1) + df_data['CD20Bcell'] = df_data.loc[:,[s_bcell,'immune']].all(axis=1) & ~df_data.loc[:,['CD68Mac',s_tcell]].any(axis=1) + df_data['TcellImmune'] = df_data.loc[:,[s_tcell,'immune']].all(axis=1) & ~df_data.loc[:,['CD20Bcell','CD68Mac']].any(axis=1) + df_data['UnspecifiedImmune'] = df_data.loc[:,'immune'] & ~df_data.loc[:,['CD20Bcell','TcellImmune','CD68Mac']].any(axis=1) + ## CD4 and CD8 + if df_data.columns.isin(['CD8_Ring','CD4_Ring']).sum()==2: + #print('CD4 AND CD8') + df_data['CD8Tcell'] = df_data.loc[: ,['CD8_Ring','TcellImmune']].all(axis=1) + df_data['CD4Tcell'] = df_data.loc[: ,['CD4_Ring','TcellImmune']].all(axis=1) & ~df_data.loc[: ,'CD8Tcell'] + df_data['UnspecifiedTcell'] = df_data.TcellImmune & ~df_data.loc[:,['CD8Tcell','CD4Tcell']].any(axis=1) #if cd4 or 8 then sum = 2 + ## check + ls_immune = df_data[df_data.loc[:,'TcellImmune']].index.tolist() + if ((df_data.loc[ls_immune,['CD8Tcell','CD4Tcell','UnspecifiedTcell']].sum(axis=1)!=1)).any(): + print('Error in Tcell cell types') + ls_immuntype = ['CD68Mac','CD20Bcell','UnspecifiedImmune','CD8Tcell','CD4Tcell','UnspecifiedTcell'] #'TcellImmune', + #add Immunetype + ls_cell_names = ls_immuntype + s_type_name = 'ImmuneType' + analyze.add_celltype(df_data, ls_cell_names, s_type_name) + + #get rid of unspecfied immune cells (make them stroma) + ls_index = df_data[df_data.ImmuneType.fillna('x').str.contains('Unspecified')].index + df_data.loc[ls_index,'celltype'] = 'stromal' + df_data.loc[ls_index,'ImmuneType'] = np.nan + df_data.loc[ls_index,'stromal'] = True + df_data.loc[ls_index,'immune'] = False + return(df_data) + +def immune_functional(df_data,ls_immune_functional): + #Immune functional states + df_data.rename(dict(zip(ls_immune_functional,[item.split('_')[0] for item in ls_immune_functional])),axis=1,inplace=True) + df_func = analyze.combinations(df_data,[item.split('_')[0] for item in ls_immune_functional]) + df_data = df_data.merge(df_func,how='left', left_index=True, right_index=True, suffixes = ('_all','')) + #gated combinations: immune type plus fuctional status + ls_gate = sorted(df_data[~df_data.ImmuneType.isna()].loc[:,'ImmuneType'].unique()) + ls_marker = df_func.columns.tolist() + df_gate_counts = analyze.gated_combinations(df_data,ls_gate,ls_marker) + df_data = df_data.merge(df_gate_counts, how='left', left_index=True, right_index=True,suffixes = ('_all','')) + #add FuncImmune + ls_cell_names = df_gate_counts.columns.tolist() + s_type_name ='FuncImmune' + analyze.add_celltype(df_data, ls_cell_names, s_type_name) + return(df_data) + +######################################## +#CellProlif combinations, main cell types and proliferation +###################################### +def cell_prolif(df_data, s_gate='celltype',ls_combo =['prolif','nonprolif']): + ls_gate = df_data.loc[:,s_gate].unique().tolist() + df_gate_counts2 = analyze.gated_combinations(df_data,ls_gate,ls_combo) + df_data = df_data.merge(df_gate_counts2, how='left', left_index=True, right_index=True,suffixes = ('_all','')) + #add CellProlif + ls_cell_names = ['endothelial_prolif','endothelial_nonprolif', 'tumor_prolif', 'tumor_nonprolif', + 'stromal_prolif', 'stromal_nonprolif', 'immune_prolif','immune_nonprolif'] + ls_cell_names = df_gate_counts2.columns.tolist() + s_type_name = 'CellProlif' + analyze.add_celltype(df_data, ls_cell_names, s_type_name) + return(df_data) + +def diff_hr_state(df_data,ls_luminal,ls_basal,ls_mes): + ls_mes = df_data.columns[(df_data.dtypes=='bool') & (df_data.columns.isin(ls_mes) | df_data.columns.isin([item.split('_')[0] for item in ls_mes]))].tolist() + print('differentiation') + df_data['Lum'] = df_data.loc[:,ls_luminal].any(axis=1) & df_data.tumor + df_data['Bas'] = df_data.loc[:,ls_basal].any(axis=1) & df_data.tumor + df_data['Mes'] = df_data.loc[:,ls_mes].any(axis=1) & df_data.tumor + + print('hormonal status') + df_data['ER'] = df_data.loc[:,['tumor','ER_Nuclei']].all(axis=1) + df_data['HER2'] = df_data.loc[:,['tumor','HER2_Ring']].all(axis=1) + ls_hr = ['ER'] + if df_data.columns.isin(['PgR_Nuclei']).any(): + df_data['PR'] = df_data.loc[:,['tumor','PgR_Nuclei']].all(axis=1) + ls_hr.append('PR') + + df_data['HR'] = df_data.loc[:,ls_hr].any(axis=1) & df_data.tumor + + ls_marker = ['Lum','Bas','Mes'] # + df_diff = analyze.combinations(df_data,ls_marker) + df_data = df_data.merge(df_diff,how='left', left_index=True, right_index=True, suffixes = ('_all','')) + + #add DiffState + ls_cell_names = df_diff.columns.tolist() + s_type_name = 'DiffState' + analyze.add_celltype(df_data, ls_cell_names, s_type_name) + #change non-tumor to NA (works!) + df_data.loc[df_data[df_data.celltype != 'tumor'].index,s_type_name] = np.nan + + #2 ER/PR/HER2 + ls_marker = ['HR','HER2'] + df_hr = analyze.combinations(df_data,ls_marker) + df_hr.rename({'__':'TN'},axis=1,inplace=True) + df_data = df_data.merge(df_hr,how='left', left_index=True, right_index=True,suffixes = ('_all','')) + ls_cell_names = df_hr.columns.tolist() + s_type_name = 'HRStatus' + analyze.add_celltype(df_data, ls_cell_names, s_type_name) + #change non-tumor to NA (works!) + df_data.loc[df_data[df_data.celltype != 'tumor'].index,s_type_name] = np.nan + + #3 combinations: differentiation and HR status + ls_gate = df_diff.columns.tolist() + ls_marker = df_hr.columns.tolist() + df_gate_counts = analyze.gated_combinations(df_data,ls_gate,ls_marker) + df_data = df_data.merge(df_gate_counts, how='left', left_index=True, right_index=True,suffixes = ('_all','')) + + # make Tumor Diff plus HR Status object column + ls_cell_names = df_gate_counts.columns.tolist() + s_type_name = 'DiffStateHRStatus' + analyze.add_celltype(df_data, ls_cell_names, s_type_name) + #change non-tumor to NA (works!) + df_data.loc[df_data[df_data.celltype != 'tumor'].index,s_type_name] = np.nan + return(df_data) + +def celltype_gates(df_data,ls_gate,s_new_name,s_celltype): + ''' + multipurpose for stromaTumor + ls_gates = + ''' + ls_gate = df_data.columns[(df_data.dtypes=='bool') & (df_data.columns.isin(ls_gate) | df_data.columns.isin([item.split('_')[0] for item in ls_gate]))].tolist() + #tumor signaling and proliferation + #rename + df_data.rename(dict(zip(ls_gate,[item.split('_')[0] for item in ls_gate])),axis=1,inplace=True) + ls_marker = [item.split('_')[0] for item in ls_gate] + #functional states (stromal) (don't forget to merge!) + df_func = analyze.combinations(df_data,ls_marker) + df_data = df_data.merge(df_func,how='left', left_index=True, right_index=True, suffixes = ('_all','')) + ls_cell_names = df_func.columns.tolist() + analyze.add_celltype(df_data, ls_cell_names, s_new_name) + #change non-tumor to NA (works!) + df_data.loc[df_data[df_data.celltype != s_celltype].index,s_new_name] = np.nan + df_data[s_new_name] = df_data.loc[:,s_new_name].replace(dict(zip(ls_cell_names,[f'{s_celltype}_{item}' for item in ls_cell_names]))) + return(df_data) + +def non_tumor(df_data): + #one more column: all non-tumor cells + index_endothelial = df_data[df_data.celltype=='endothelial'].index + index_immune = df_data[df_data.celltype=='immune'].index + index_stroma = df_data[df_data.celltype=='stromal'].index + index_tumor = df_data[df_data.celltype=='tumor'].index + + if df_data.columns.isin(['ImmuneType','StromalType']).sum() == 2: + #fewer cell tpyes + df_data.loc[index_endothelial,'NonTumor'] = 'endothelial' + df_data.loc[index_immune,'NonTumor'] = df_data.loc[index_immune,'ImmuneType'] + df_data.loc[index_stroma,'NonTumor'] = df_data.loc[index_stroma,'StromalType'] + df_data.loc[index_tumor,'NonTumor'] = np.nan + + if df_data.columns.isin(['FuncImmune','CellProlif']).sum() == 2: + #more cell types + df_data.loc[index_endothelial,'NonTumorFunc'] = df_data.loc[index_endothelial,'CellProlif'] + df_data.loc[index_immune,'NonTumorFunc'] = df_data.loc[index_immune,'FuncImmune'] + df_data.loc[index_stroma,'NonTumorFunc'] = df_data.loc[index_stroma,'StromalType'] + df_data.loc[index_tumor,'NonTumorFunc'] = np.nan + return(df_data) diff --git a/mplex_image/getdata.py b/mplex_image/getdata.py new file mode 100755 index 0000000..aca70dc --- /dev/null +++ b/mplex_image/getdata.py @@ -0,0 +1,176 @@ +#### +# title: getdata.py +# +# language: Python3.6 +# date: 2018-08-00 +# license: GPL>=v3 +# author: Jenny, bue (mostly bue) +# +# description: +# python3 library to analyise guillaume segemented cyclic staining data. +#### + +# load library +import csv +import os +import re + + +# function implementaion +# import importlib +# importlib.reload(getdata) + +def get_df( + #s_gseg_folder_root='/graylab/share/engje/Data/', + #s_scene_label='Registered-Her' + s_folder_regex="^SlideName.*_Features$", + es_value_label = {"MeanIntensity","CentroidX","CentroidY"}, + #s_df_folder_root="./", + #b_roundscycles=False, + ): + ''' + input: + segmentation fiels from Guillaume's software, which have in the + "Label" column the "cell serial number" (cell) + and in other columns the "feature of intrests" and unintrest. + + the segmentation files are ordered in such a path structure: + + {s_gseg_folder_root} + |+ {s_gseg_folder_run_regex}*_YYYY-MM-DD_* (run) + | |+ Scene 000 - Nuclei - CD32.txt (scene and protein) + | |+ Scene 000 - Location - ProteinName.txt + | + |+ {s_gseg_folder_run_regex}*_YYYY-MM-DD_* + + output: + at {s_df_folder_root} tab separated value dataframe files + per run and feature of intrest. + y-axis: protein_location + x-axis: scene_cell + + runYYYYMMDD_MeanIntensity.tsv + + runYYYYMMDD_{s_gseg_feature_label}.tsv + + run: + import getdata + getdata.get_df(s_gseg_folder_root='ihcData', s_gseg_folder_run_regex='^BM-Her2N75') + + description: + function to extrtact dataframe like files of features of intrest + from segmentation files from guilaumes segmentation software. + ''' + # enter the data path + #os.chdir(s_gseg_folder_root) + + + # for each value label of intrest (such as MeanIntensity) + for s_value_label in es_value_label: + + # for each run (such as folder BM-Her2N75-15_2017-08-07_Features) + # change re.search to somehow specify folder of interest + for s_dir in os.listdir(): + if re.search(s_folder_regex, s_dir): + print(f"\nprocess {s_value_label} run: {s_dir}") + # enter the run directory + os.chdir(s_dir) + # extract run label from dir name + s_run = f"features_{s_dir.split('_')[0]}" + # get empty run dictionary + dd_run = {} + + # for each data file + for s_file in os.listdir(): + if re.search("^Scene", s_file): + print(f"process {s_value_label} file: {s_file} ...") + # extract scene from file name + ls_file = [s_splinter.strip() for s_splinter in s_file.split("-")] + s_scene = re.sub("[^0-9a-zA-Z]", "", ls_file[0].lower()) #take out any alpha numberic + # extract protein from file name + if (len(ls_file) < 3): + s_protein = f"{ls_file[1].split('.')[0]}" # this is dapi + else: + s_protein = f"{ls_file[2].split('.')[0]}_{ls_file[1]}" # others + + # for each datarow in file + b_header = False # header row inside file not yet found, so set flag false + with open(s_file, newline='') as f_csv: + o_reader = csv.reader(f_csv, delimiter=' ', quotechar='"') + for ls_row in o_reader: + if (b_header): + # extract cell label and data vale + s_cell = ls_row[i_xcell] + s_cell = f"{'0'*(5 - len(s_cell))}{s_cell}" + o_value = ls_row[i_xvalue] + # update run dictionary via scene_cell dictionery (one scene_cell dictionary per dataframe row) + s_scene_cell = f"{s_scene}_cell{s_cell}" + try: + d_scene_cell = dd_run[s_scene_cell] # we have already some data from this scene_cell + except KeyError: + d_scene_cell = {} # this is the first time we deal with this scene_cell + # update scene_cell dictionary with data values (one value inside dataframe row) + try: + o_there = d_scene_cell[s_protein] + sys.exit(f"Error @ getDataframe : in run {s_run} code tries to populate dataframe row {s_scene_cell} column {s_protein} with a secound time (there:{o_there} new:{o_value}). this should never happen. code is messed up.") + except KeyError: + d_scene_cell.update({s_protein: o_value}) + dd_run.update({s_scene_cell: d_scene_cell}) + else: + # extract cell label and data value of intrest column position + i_xcell = ls_row.index("Label") + i_xvalue = ls_row.index(s_value_label) + b_header = True # header row found and information extracted, so set flag True + + # write run dictionar of dictionary into dataframe like file + b_header = False + s_file_output = f"../{s_run}_{s_value_label}.tsv" + print(f"write file: {s_file_output}") + with open(s_file_output, 'w', newline='') as f: + for s_scene_cell in sorted(dd_run): + ls_datarow = [s_scene_cell] + # handle protein column label row + if not (b_header): + ls_protein = sorted(dd_run[s_scene_cell]) + print(ls_protein) + f.write("\t" + "\t".join(ls_protein) + "\n") + b_header = True + # handle data row + for s_protein in ls_protein: + o_value = dd_run[s_scene_cell][s_protein] + ls_datarow.append(o_value) + f.write("\t".join(ls_datarow) + "\n") + # sanity check + if (len(ls_protein) != (len(ls_datarow) -1)): + sys.exit(f"Error @ getDataframe : at {s_scene_cell} there are {len(ls_datarow) - len(ls_protein) -1} more proteins then in the aready writen rows") + + # jump back to the data path + os.chdir("..") + + return(dd_run) + + +def dfextract(df_origin, s_extract, axis=0): + ''' + input: + df_origin: dataframe + s_extract: index or column marker to be extacted + axis: 0 specifies index to be extracted, + 1 specifies columns to be extracted + + output: + df_extract: extracted dataframe + + run: + import cycnorm + cycnorm.dfyextract(df_scene, s_extract='CD74') + cycnorm.dfextract(df_run, s_scene='scene86') + + description: + function can extract e.g. + specific scene datafarme from gseg2df generated run datafarme or + specific protein from a scene dataframe. + ''' + if (axis == 0): + df_extract = df_origin.loc[df_origin.index.str.contains(s_extract),:] + else: + df_extract = df_origin.loc[:,df_origin.columns.str.contains(s_extract)] + # output + return(df_extract) diff --git a/mplex_image/imagine.py b/mplex_image/imagine.py new file mode 100755 index 0000000..f705318 --- /dev/null +++ b/mplex_image/imagine.py @@ -0,0 +1,504 @@ +### +# title: pysci.imagine.py +# +# language Python3 +# license: GPLv3 +# author: bue +# date: 2019-01-31 +# +# run: +# form pysci import imagine +# +# description: +# my image analysis library +#### + +# library +import numpy as np +import pandas as pd + +# function +def slide_up(a): + """ + input: + a: numpy array + + output: + a: input numpy array shifted one row up. + top row get deleted, + bottom row of zeros is inserted. + + description: + inspired by np.roll function, though elements that roll + beyond the last position are not re-introduced at the first. + """ + a = np.delete(np.insert(a, -1, 0, axis=0), 0, axis=0) + return(a) + + +def slide_down(a): + """ + input: + a: numpy array + + output: + a: input numpy array shifted one row down. + top row of zeros is inserted. + bottom row get deleted, + + description: + inspired by np.roll function, though elements that roll + beyond the last position are not re-introduced at the first. + """ + a = np.delete(np.insert(a, 0, 0, axis=0), -1, axis=0) + return(a) + + +def slide_left(a): + """ + input: + a: numpy array + + output: + a: input numpy array shifted one column left. + left most column gets deleted, + right most a column of zeros is inserted. + + description: + inspired by np.roll function, though elements that roll + beyond the last position are not re-introduced at the first. + """ + a = np.delete(np.insert(a, -1, 0, axis=1), 0, axis=1) + return(a) + + +def slide_right(a): + """ + input: + a: numpy array + + output: + a: input numpy array shifted one column right. + left most a column of zeros is inserted. + right most column gets deleted, + + description: + inspired by np.roll function, though elements that roll + beyond the last position are not re-introduced at the first. + """ + a = np.delete(np.insert(a, 0, 0, axis=1), -1, axis=1) + return(a) + + +def slide_upleft(a): + """ + input: + a: numpy array + + output: + a: input numpy array shifted one row up and one column left. + + description: + inspired by np.roll function. + """ + a = slide_left(slide_up(a)) + return(a) + + +def slide_upright(a): + """ + input: + a: numpy array + + output: + a: input numpy array shifted one row up and one column right. + + description: + inspired by np.roll function. + """ + a = slide_right(slide_up(a)) + return(a) + + +def slide_downleft(a): + """ + input: + a: numpy array + + output: + a: input numpy array shifted one row down and one column left. + + description: + inspired by np.roll function. + """ + a = slide_left(slide_down(a)) + return(a) + + +def slide_downright(a): + """ + input: + a: numpy array + + output: + a: input numpy array shifted one row down and one column right. + + description: + inspired by np.roll function. + """ + a = slide_right(slide_down(a)) + return(a) + + + +def get_border(ai_basin): + """ + input: + ai_basin: numpy array representing a cells or nuclei basin file. + it is assumed that basin borders are represented by 0 values, + and basins are represented with any values different from 0. + ai_basin = skimage.io.imread("cells_basins.tif") + + output: + ai_border: numpy array containing only the cell or nuclei basin border. + border value will be 1, non border value will be 0. + + description: + algorithm to extract the basin borders form basin numpy arrays. + """ + ab_border_up = (ai_basin - slide_up(ai_basin)) != 0 + ab_border_down = (ai_basin - slide_down(ai_basin)) != 0 + ab_border_left = (ai_basin - slide_left(ai_basin)) != 0 + ab_border_right = (ai_basin - slide_right(ai_basin)) != 0 + ab_border_upleft = (ai_basin - slide_upleft(ai_basin)) != 0 + ab_border_upright = (ai_basin - slide_upright(ai_basin)) != 0 + ab_border_downleft = (ai_basin - slide_downleft(ai_basin)) != 0 + ab_border_downright = (ai_basin - slide_downright(ai_basin)) != 0 + ab_border = ab_border_up | ab_border_down | ab_border_left | ab_border_right | ab_border_upleft | ab_border_upright | ab_border_downleft | ab_border_downright + ai_border = ab_border * 1 + return(ai_border) + + +def collision(ai_basin, i_step_size=1): + """ + input: + ai_basin: numpy array representing a cells basin file. + it is assumed that basin borders are represented by 0 values, + and basins are represented with any values different from 0. + ai_basin = skimage.io.imread("cells_basins.tif") + + i_step_size: integer that specifies the distance from a basin + where collisions with other basins are detected. + increasing the step size behind > 1 will result in faster processing + but less certain results. step size < 1 make no sense. + default step size is 1. + + output: + eti_collision: a set of tuples representing colliding basins. + + description: + algorithm to detect which basin collide a given step size away. + """ + eti_collision = set() + for o_slide in {slide_up, slide_down, slide_left, slide_right, slide_upleft, slide_upright, slide_downleft, slide_downright}: + ai_walk = ai_basin.copy() + for _ in range(i_step_size): + ai_walk = o_slide(ai_walk) + ai_alice = ai_walk[(ai_basin != 0) & (ai_walk != 0)] + ai_bob = ai_basin[(ai_basin != 0) & (ai_walk != 0)] + eti_collision = eti_collision.union(set( + zip( + ai_alice[(ai_alice != ai_bob)], + ai_bob[(ai_bob != ai_alice)] + ) + )) + # return + return(eti_collision) + + +def grow(ai_basin, i_step=1): + """ + input: + ai_basin: numpy array representing a cells basin file. + it is assumed that basin borders are represented by 0 values, + and basins are represented with any values different from 0. + ai_basin = skimage.io.imread("cells_basins.tif") + + i_step: integer which specifies how many pixels the basin + to each direction should grow + + output: + ai_grown: numpy array with the grown basins + + description: + algorithm to grow the basis in a given basin numpy array. + growing happens counterclockwise. + """ + ai_grown = ai_basin.copy() + for _ in range(i_step): + for o_slide in {slide_up, slide_upleft, slide_left, slide_downleft, slide_down, slide_downright, slide_right, slide_upright}: + ai_alice = ai_basin.copy() + ai_evolve = o_slide(ai_alice) + ai_alice[(ai_evolve != ai_alice) & (ai_alice == 0)] = ai_evolve[(ai_evolve != ai_alice) & (ai_alice == 0)] + # update grown + ai_grown[(ai_alice != ai_grown) & (ai_grown == 0)] = ai_alice[(ai_alice != ai_grown) & (ai_grown == 0)] + # output + return(ai_grown) + + +def touching_cells(ai_basin, i_border_width=0, i_step_size=1): + """ + input: + ai_basin: numpy array representing a cells basin file. + it is assumed that basin borders are represented by 0 values, + and basins are represented with any values different from 0. + ai_basin = skimage.io.imread("cells_basins.tif") + + i_border_width: maximal acceptable border with in pixels. + this is half of the range how far two the adjacent cell maximal + can be apart and still are regarded as touching each other. + + i_step_size: step size by which the border width is sampled for + touching cells. + increase the step size behind > 1 will result in faster processing + but less certain results. step size < 1 make no sense. + default step size is 1. + + output: + dei_touch: a dictionary that for each basin states + which other basins are touching. + + description: + algorithm to extract the touching basins from a cell basin numpy array. + algorithm inspired by C=64 computer games with sprit collision. + """ + + # detect neighbors + eti_collision = set() + ai_evolve = ai_basin.copy() + for _ in range(-1, i_border_width, i_step_size): + # detect cell border collision + eti_collision = eti_collision.union( + collision(ai_basin=ai_evolve, i_step_size=i_step_size) + ) + # grow basin + ai_evolve = grow(ai_basin=ai_evolve, i_step=i_step_size) + + # transform set of tuple of alice and bob collision to dictionary of sets + dei_touch = {} + ei_alice = set(np.ndarray.flatten(ai_basin)) + ei_alice.remove(0) + for i_alice in ei_alice: + dei_touch.update({i_alice : set()}) + for i_alice, i_bob in eti_collision: + ei_bob = dei_touch[i_alice] + ei_bob.add(i_bob) + dei_touch.update({i_alice : ei_bob}) + + # output + return(dei_touch) + + +def detouch2df(deo_abc, ls_column=["cell_center","cell_touch"]): + """ + input: + deo_touch: touching_cells generated dictionary + ls_column: future dictionary_key dictionary_value column name + + output: + df_touch: dataframe which contains the same information + as the input deo_touch dictionary. + + description: + transforms dei_touch dictionary into a two column dataframe. + """ + lo_key_total= [] + lo_value_total = [] + for o_key, eo_value in deo_abc.items(): + try: + lo_value = sorted(eo_value, key=int) + except ValueError: + lo_value = sorted(eo_value) + # extract form dictionary + if (len(lo_value) == 0): + lo_key_total.append(o_key) + lo_value_total.append(0) + else: + lo_key_total.extend([o_key] * len(lo_value)) + lo_value_total.extend(lo_value) + # generate datafarme + df_touch = pd.DataFrame([lo_key_total,lo_value_total], index=ls_column).T + return(df_touch) + + +def imgfuse(laaai_in): + """ + input: + laaai_in: list of 3 channel (RGB) images + + output: + aaai_out: fused 3 channel image + + description: + code to fuse many RGB images into one. + """ + # check shape + ti_shape = None + for aaai_in in laaai_in: + if (ti_shape is None): + ti_shape = aaai_in.shape + else: + if (aaai_in.shape != ti_shape): + sys.exit(f"Error: input images have not the same shape. {aaai_in.shape} != {aaai_in}.") + + # fuse images + llli_channel = [] + for i_channel in range(ti_shape[0]): + lli_matrix = [] + for i_y in range(ti_shape[1]): + li_row = [] + for i_x in range(ti_shape[2]): + #print(f"{i_channel} {i_y} {i_x}") + li_px = [] + for aaai_in in laaai_in: + i_in = aaai_in[i_channel,i_y,i_x] + if (i_in != 0): + li_px.append(i_in) + if (len(li_px) != 0): + i_out = np.mean(li_px) + else: + i_out = 0 + li_row.append(int(i_out)) + lli_matrix.append(li_row) + llli_channel.append(lli_matrix) + + # output + aaai_out = np.array(llli_channel) + return(aaai_out) + + + +# test code +if __name__ == "__main__": + + # load basins tiff into numpy array + ''' + import matplotlib.pyplot as plt + import skimage as ski + a_tiff = ski.io.imread("cells_basins.tif") + plt.imshow(a_tiff) + ''' + + # generate test data + a = np.array([ + [0,0,0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,4,0,0,0], + [0,0,0,1,1,1,0,0,0,0,0,0,0,0], + [0,0,0,1,1,1,0,0,0,0,0,0,0,0], + [0,0,0,1,1,1,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,2,2,2,0,0,0], + [0,0,0,0,3,3,3,0,2,2,2,0,0,0], + [0,0,0,0,3,3,3,0,2,2,2,0,0,0], + [0,0,0,0,3,3,3,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0,0,0], + ]) + + b = np.array([ + [0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,1,0,0,0,0,0,0,0], + [0,0,0,0,1,2,0,0,0,0,0], + [0,0,0,0,0,1,2,0,0,0,0], + [0,0,0,0,0,0,0,2,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0], + ]) + + c = np.array([ + [0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,1,0,0,0,0,0], + [0,0,0,0,0,1,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0], + ]) + + # run get_border + print("\nborderwall_tm") + print(a) + print(get_border(a)) + #plt.imshow(get_border(a_tiff)) + + # run grow + ''' + print("\ngrow") + print(c) + print(grow(c)) + print(grow(grow(c))) + print(grow(c, i_step_size=2)) + print(b) + print(grow(b)) + print(grow(grow(b))) + print(grow(b, i_step_size=2)) + ''' + + # run collision + ''' + print("\ncollision") + print(c) + print(collision(c)) + print(b) + print(collision(b)) + print(c) + print(collision(c)) + ''' + + # run touching_cells + print("\ntouch") + #print(a) + print(touching_cells(a, i_border_width=0)) + print(touching_cells(a, i_border_width=1)) + print(touching_cells(a, i_border_width=2)) + print(touching_cells(a, i_border_width=3)) + print(touching_cells(a, i_border_width=4)) + print(touching_cells(a, i_border_width=4, i_step_size=2)) + #touching_cells(a_tiff, i_border_width=1) + + + # img fuse + aaai_1 = np.array([ + [[1,1,1],[2,2,2],[3,3,3]], + [[0,0,0,],[0,0,0],[0,0,0]], + [[0,0,0],[0,0,0],[0,0,0]], + ]) + aaai_2 = np.array([ + [[0,0,0,],[0,0,0],[0,0,0]], + [[1,1,1],[2,2,2],[3,3,3]], + [[0,0,0],[0,0,0],[0,0,0]], + ]) + aaai_3 = np.array([ + [[0,0,0,],[0,0,0],[0,0,0]], + [[0,0,0],[0,0,0],[0,0,0]], + [[1,1,1],[2,2,2],[3,3,3]], + ]) + aaai_4 = np.array([ + [[1,1,1],[2,2,2],[3,3,3]], + [[1,1,1],[2,2,2],[3,3,3]], + [[0,0,0],[0,0,0],[0,0,0]], + ]) + aaai_5 = np.array([ + [[0,0,0,],[0,0,0],[0,0,0]], + [[1,1,1],[2,2,2],[3,3,3]], + [[1,1,1],[2,2,2],[3,3,3]], + ]) + aaai_out = imgfuse([aaai_1, aaai_2, aaai_3, aaai_4, aaai_5]) + print("fused 3channel image:\n", aaai_out, type(aaai_out)) diff --git a/mplex_image/metadata.py b/mplex_image/metadata.py new file mode 100755 index 0000000..4d49424 --- /dev/null +++ b/mplex_image/metadata.py @@ -0,0 +1,176 @@ +#### +# title: metadata.py +# +# language: Python3.7 +# date: 2020-07-00 +# license: GPL>=v3 +# author: Jenny +# +# description: +# python3 library using python bioformats to extract image metadata +#### + + +#libraries +import matplotlib as mpl +mpl.use('agg') +import matplotlib.pyplot as plt +import numpy as np +import os +import skimage +import pandas as pd +import bioformats +#import javabridge +import re +import shutil +from itertools import chain, compress +import matplotlib.ticker as ticker +from mplex_image import cmif + +# mpimage +#functions + +def get_exposure(s_image, s_find="Information\|Image\|Channel\|ExposureTime\<\/Key\>\"): + + s_meta = bioformats.get_omexml_metadata(path=s_image) + o = bioformats.OMEXML(s_meta) + print(o.image().Name) + print(o.image().AcquisitionDate) + + li_start = [m.start() for m in re.finditer(s_find, s_meta)] + if len(li_start)!=1: + print('Error: found wrong number of exposure times') + + ls_exposure = [] + for i_start in li_start: + ls_exposure.append(s_meta[i_start:i_start+200]) + s_exposure = ls_exposure[0].strip(s_find) + s_exposure = s_exposure[1:s_exposure.find(']')] + ls_exposure = s_exposure.split(',') + li_exposure = [int(item)/1000000 for item in ls_exposure] + return(li_exposure,s_meta) + +def get_exposure_sample(s_sample,df_img): + """ + return a dataframe with all exposure times for a sample (slide) + """ + #make dataframe of exposure time metadata + df_exposure = pd.DataFrame() + ls_image = os.listdir() + df_sample = df_img[df_img.index.str.contains(s_sample)] + for s_image in df_sample.index: + print(s_image) + li_exposure, s_meta = get_exposure(s_image) + se_times = pd.Series(li_exposure,name=s_image) + df_exposure = df_exposure.append(se_times) + return(df_exposure) + +def get_meta(s_image, s_find = 'Scene\|CenterPosition\<\/Key\>\\['): + """czi scene metadata + s_image = filename + s_find = string to find in the omexml metadata + returns: + ls_exposure = list of 200 character strings following s_find in metadata + s_meta = the whole metadata string + """ + s_meta = bioformats.get_omexml_metadata(path=s_image) + o = bioformats.OMEXML(s_meta) + #print(o.image().Name) + #print(o.image().AcquisitionDate) + + li_start = [m.start() for m in re.finditer(s_find, s_meta)] + if len(li_start)!=1: + print('Error: found wrong number of exposure times') + + ls_exposure = [] + for i_start in li_start: + ls_exposure.append(s_meta[i_start:i_start+200]) + s_exposure = ls_exposure[0].strip(s_find) + s_exposure = s_exposure[0:s_exposure.find(']')] + ls_exposure = s_exposure.split(',') + #li_exposure = [int(item)/1000000 for item in ls_exposure] + return(ls_exposure,s_meta) + +def scene_position(czidir,type): + """ + get a dataframe of scene positions for each round/scene in TMA + """ + os.chdir(f'{czidir}') + df_img = cmif.parse_czi('.',type=type) + + #javabridge.start_vm(class_path=bioformats.JARS) + for s_image in df_img.index: + print(s_image) + ls_exposure,s_meta = get_meta(s_image) + df_img.loc[s_image,'Scene_X'] = ls_exposure[0] + df_img.loc[s_image,'Scene_Y'] = ls_exposure[1] + + #javabridge.kill_vm() + + df_img = df_img.sort_values(['rounds','scanID','scene']).drop('data',axis=1) + return(df_img) + + + ls_exposure,s_meta = get_meta(s_image, s_find = 'Scene\|CenterPosition\<\/Key\>\\[') + +def exposure_times_scenes(df_img,codedir,czidir,s_end='.czi'): + """ + get a csv of exposure times for each slide + """ + #go to directory + os.chdir(czidir) + #export exposure time + s_test = sorted(compress(os.listdir(),[item.find(s_end) > -1 for item in os.listdir()]))[1]#[0] + s_find = f"{s_test.split('-Scene-')[1].split(s_end)[0]}" + for s_sample in sorted(set(df_img.slide)): + print(s_sample) + df_img_slide = df_img[(df_img.slide==s_sample) & (df_img.scene==s_find)] + print(len(df_img_slide)) + df_exp = get_exposure_sample(s_sample,df_img_slide) + df_exp.to_csv(f'{codedir}/{s_sample}_ExposureTimes.csv',header=True,index=True) + +def exposure_times(df_img,codedir,czidir): + """ + get a csv of exposure times for each slide + """ + #go to directory + os.chdir(czidir) + print(czidir) + #export exposure time + for s_sample in sorted(set(df_img.slide)): + df_img_slide = df_img[df_img.slide==s_sample] + df_exp = get_exposure_sample(s_sample,df_img_slide) + df_exp.to_csv(f'{codedir}/{s_sample}_ExposureTimes.csv',header=True,index=True) + #close java virtual machine + #javabridge.kill_vm() + +def exposure_times_slide(df_img,codedir,czidir): + if len(df_img.scene.unique()) == 1: + exposure_times(df_img,codedir,czidir) + elif len(df_img.scene.unique()) > 1: + exposure_times_scenes(df_img,codedir,czidir,s_end='.czi') + +def export_tiffs(df_img, s_sample,tiffdir): + """ + export the tiffs of each tile + """ + #start java virtual machine + #javabridge.start_vm(class_path=bioformats.JARS) + + #export tiffs + df_img_slide = df_img[df_img.slide==s_sample] + for path in df_img_slide.index: + print(path) + img = bioformats.load_image(path) #looks like it only loads the first tile + img_new = img*65535 + img_16 = img_new.astype(np.uint16) + i_channels = img_16.shape[2] + for i_channel in range(i_channels): + print(f'channel {i_channel}') + bioformats.write_image(f'{tiffdir}/{path.split(".czi")[0]}_c{str(i_channel+1)}_ORG.tif', pixels=img_16[:,:,i_channel],pixel_type='uint16') + break + break + a_test = img_16[:,:,i_channel] + aa_test = img_16 + #javabridge.kill_vm() + return(a_test,aa_test, img) diff --git a/mplex_image/mics.py b/mplex_image/mics.py new file mode 100755 index 0000000..d16b479 --- /dev/null +++ b/mplex_image/mics.py @@ -0,0 +1,581 @@ +# wrapper functions for codex image processing + +from mplex_image import preprocess, mpimage, getdata, process, analyze, cmif, features, ometiff +import os +import pandas as pd +import math +import skimage +from skimage import io, filters +import re +import numpy as np +import json +from skimage.util import img_as_uint +import tifffile + +def parse_processed(): + ''' + parse the file names of processed Macsima images + ''' + df_img = mpimage.filename_dataframe(s_end ="ome.tif",s_start='R',s_split='___') + #standardize dapi naming + ls_dapi_index = df_img[df_img.index.str.contains('DAPI')].index.tolist() + d_replace = dict(zip(ls_dapi_index, [item.replace('DAPIV0','DAPI__DAPIV0') for item in ls_dapi_index])) + df_img['data'] = df_img.data.replace(d_replace) + #standardize AF naming + ls_dapi_index = df_img[df_img.index.str.contains('autofluorescence')].index.tolist() + d_replace = dict(zip(ls_dapi_index, [item.replace('autofluorescence_FITC','autofluorescence-FITC__FITC') for item in ls_dapi_index])) + df_img['data'] = df_img.data.replace(d_replace) + d_replace = dict(zip(ls_dapi_index, [item.replace('autofluorescence_PE','autofluorescence-PE__PE') for item in ls_dapi_index])) + df_img['data'] = df_img.data.replace(d_replace) + #standardize empty naming + ls_dapi_index = df_img[df_img.index.str.contains('empty')].index.tolist() + d_replace = dict(zip(ls_dapi_index, [item.replace('empty','empty__empty') for item in ls_dapi_index])) + df_img['data'] = df_img.data.replace(d_replace) + df_img['marker'] = [item.split(f"{item.split('_')[3]}_")[-1].split('__')[0] for item in df_img.data] + df_img['cycle'] = [item.split('_')[3] for item in df_img.data] + df_img['rounds'] = [item.split('_')[3].replace('C-','R') for item in df_img.data] + df_img['clone'] = [item.split('__')[1].split('.')[0] for item in df_img.data] + #standardize marker naming + d_replace = dict(zip(df_img.marker.tolist(),[item.replace('_','-') for item in df_img.marker.tolist()])) + df_img['data'] = [item.replace(f'''{item.split(f"{item.split('_')[3]}_")[-1].split('__')[0]}''',f'''{d_replace[item.split(f"{item.split('_')[3]}_")[-1].split('__')[0]]}''') for item in df_img.data] + df_img['exposure'] = [int(item.split('__')[1].split('_')[1].split('.')[0]) for item in df_img.data] + df_img['channel'] = [item.split('__')[1].split('_')[0].split('.')[1] for item in df_img.data] + d_replace = {'DAPI':'c1', 'FITC':'c2', 'PE':'c3', 'APC':'c4'} + df_img['color'] = [item.replace(item, d_replace[item]) for item in df_img.channel] + df_img['rack'] = [item.split('_')[0] for item in df_img.data] + df_img['slide'] = [item.split('_')[1] for item in df_img.data] + df_img['scene'] = [item.split('_')[2] for item in df_img.data] + return(df_img) + +def parse_org(): + ''' + parse the file names of copied (name-stadardized) Macsima images + ''' + s_path = os.getcwd() + df_img = mpimage.filename_dataframe(s_end ="tif",s_start='R',s_split='___') + df_img['marker'] = [item.split(f"{item.split('_')[3]}_")[-1].split('__')[0] for item in df_img.data] + df_img['cycle'] = [item.split('_')[3] for item in df_img.data] + df_img['rounds'] = [item.split('_')[3].replace('C-','R') for item in df_img.data] + df_img['clone'] = [item.split('__')[1].split('.')[0] for item in df_img.data] + df_img['exposure'] = [int(item.split('__')[1].split('_')[1].split('.')[0]) for item in df_img.data] + df_img['channel'] = [item.split('__')[1].split('_')[0].split('.')[1] for item in df_img.data] + d_replace = {'DAPI':'c1', 'FITC':'c2', 'PE':'c3', 'APC':'c4'} + df_img['color'] = [item.replace(item, d_replace[item]) for item in df_img.channel] + df_img['rack'] = [item.split('_')[0] for item in df_img.data] + df_img['slide'] = [item.split('_')[1] for item in df_img.data] + df_img['scene'] = [item.split('_')[2] for item in df_img.data] + df_img['slide_scene'] = df_img.slide + '_' + df_img.scene + df_img['path'] = [f"{s_path}/{item}" for item in df_img.index] + return(df_img) + +def copy_processed(df_img,regdir,i_lines=32639): + ''' + copy the highest exposure time images for processing + ''' + for s_marker in sorted(set(df_img.marker) - {'DAPI','autofluorescence','empty'}): + df_marker = df_img[df_img.marker==s_marker] + for s_cycle in sorted(set(df_marker.cycle)): + for s_index in df_marker[df_marker.cycle==s_cycle].sort_values('exposure',ascending=False).index.tolist(): + a_img = io.imread(s_index) + s_dir_new = s_index.split(f"_{df_img.loc[s_index,'cycle']}")[0] + s_index_new = df_img.loc[s_index,'data'].split('.ome.tif')[0] + preprocess.cmif_mkdir([f'{regdir}/{s_dir_new}']) + print(a_img.max()) + #get rid of lines + a_img[a_img==i_lines] = a_img.min() + if a_img.max() < 65535: + io.imsave(f'{regdir}/{s_dir_new}/{s_index_new}.tif',a_img,plugin='tifffile',check_contrast=False) + break + else: + print('Try lower exposure time') + for s_index in df_img[df_img.marker=='DAPI'].index.tolist(): + a_img = io.imread(s_index) + print(f'DAPI max: {a_img.max()}') + if df_img.loc[s_index,'rounds'] != 'R0': #keep lines in R0 dapi, for segmentation + a_img[a_img==i_lines] = a_img.min() + s_dir_new = s_index.split(f"_{df_img.loc[s_index,'cycle']}")[0] + s_index_new = df_img.loc[s_index,'data'].split('.ome.tif')[0] + preprocess.cmif_mkdir([f'{regdir}/{s_dir_new}']) + io.imsave(f'{regdir}/{s_dir_new}/{s_index_new}.tif',a_img,plugin='tifffile',check_contrast=False) + +def extract_cellpose_features(s_sample, segdir, regdir, ls_seg_markers, nuc_diam, cell_diam): + ''' + load the segmentation results, the input images, and the channels images + extract mean intensity from each image, and centroid, area and eccentricity for + ''' + df_sample = pd.DataFrame() + df_thresh = pd.DataFrame() + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation') + ls_scene = [] + d_match = {} + for s_file in os.listdir(): + if s_file.find(f'{".".join(ls_seg_markers)} nuc{nuc_diam} matchedcell{cell_diam} - Cell Segmentation Basins')>-1: + ls_scene.append(s_file.split(f'_{".".join(ls_seg_markers)}')[0]) + d_match.update({s_file.split(f'_{".".join(ls_seg_markers)}')[0]:s_file}) + for s_scene in ls_scene: + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation') + print(f'processing {s_scene}') + for s_file in os.listdir(): + if s_file.find(s_scene) > -1: + if s_file.find("DAPI.png") > -1: + s_dapi = s_file + dapi = io.imread(f'{segdir}/{s_sample}Cellpose_Segmentation/{s_dapi}') + print(f'loading {s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif') + labels = io.imread(f'{s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif') + cell_labels = io.imread(f'{segdir}/{s_sample}Cellpose_Segmentation/{d_match[s_scene]}') + print(f'loading {d_match[s_scene]}') + #nuclear features + df_feat = features.extract_feat(labels,dapi, properties=(['label'])) + df_feat.columns = [f'{item}_segmented-nuclei' for item in df_feat.columns] + df_feat.index = [f'{s_sample}_cell{item}' for item in df_feat.loc[:,'label_segmented-nuclei']] + + #get subcellular regions + cyto = features.label_difference(labels,cell_labels) + d_loc_nuc = features.subcellular_regions(labels, distance_short=2, distance_long=5) + d_loc_cell = features.subcellular_regions(cell_labels, distance_short=2, distance_long=5) + d_loc = {'nuclei':labels,'cell':cell_labels,'cytoplasm':cyto, + 'nucmem':d_loc_nuc['membrane'][0],'cellmem':d_loc_cell['membrane'][0], + 'perinuc5':d_loc_nuc['ring'][1],'exp5':d_loc_nuc['grown'][1], + 'nucadj2':d_loc_nuc['straddle'][0],'celladj2':d_loc_cell['straddle'][0]} + + #subdir organized by slide or scene + if os.path.exists(f'{regdir}/{s_sample}'): + os.chdir(f'{regdir}/{s_sample}') + elif os.path.exists(f'{regdir}/{s_scene}'): + os.chdir(f'{regdir}/{s_scene}') + else: + os.chdir(f'{regdir}') + df_img = parse_org() + df_img['round_int'] = [int(re.sub('[^0-9]','', item)) for item in df_img.rounds] + df_img = df_img[df_img.round_int < 90] + df_img = df_img.sort_values('round_int') + #take into account slide (well) + df_scene = df_img[df_img.slide_scene==s_scene] + #load each image + for s_index in df_scene.index: + intensity_image = io.imread(s_index) + df_thresh.loc[s_index,'threshold_li'] = filters.threshold_li(intensity_image) + if intensity_image.mean() > 0: + df_thresh.loc[s_index,'threshold_otsu'] = filters.threshold_otsu(intensity_image) + df_thresh.loc[s_index,'threshold_triangle'] = filters.threshold_triangle(intensity_image) + s_marker = df_scene.loc[s_index,'marker'] + print(f'extracting features {s_marker}') + if s_marker == 'DAPI': + s_marker = s_marker + f'{df_scene.loc[s_index,"rounds"].split("R")[1]}' + for s_loc, a_loc in d_loc.items(): + if s_loc == 'nuclei': + df_marker_loc = features.extract_feat(a_loc,intensity_image, properties=(['mean_intensity','centroid','area','eccentricity','label'])) + df_marker_loc.columns = [f'{s_marker}_{s_loc}',f'{s_marker}_{s_loc}_centroid-0',f'{s_marker}_{s_loc}_centroid-1',f'{s_marker}_{s_loc}_area',f'{s_marker}_{s_loc}_eccentricity',f'{s_marker}_{s_loc}_label'] + elif s_loc == 'cell': + df_marker_loc = features.extract_feat(a_loc,intensity_image, properties=(['mean_intensity','euler_number','area','eccentricity','label'])) + df_marker_loc.columns = [f'{s_marker}_{s_loc}',f'{s_marker}_{s_loc}_euler',f'{s_marker}_{s_loc}_area',f'{s_marker}_{s_loc}_eccentricity',f'{s_marker}_{s_loc}_label'] + else: + df_marker_loc = features.extract_feat(a_loc,intensity_image, properties=(['mean_intensity','label'])) + df_marker_loc.columns = [f'{s_marker}_{s_loc}',f'{s_marker}_{s_loc}_label'] + #set array ids as index + df_marker_loc.index = df_marker_loc.loc[:,f'{s_marker}_{s_loc}_label'] + df_marker_loc.index = [f'{s_sample}_cell{item}' for item in df_marker_loc.index] + df_feat = df_feat.merge(df_marker_loc, left_index=True,right_index=True,how='left',suffixes=('',f'{s_marker}_{s_loc}')) + df_sample = df_sample.append(df_feat) + return(df_sample, df_thresh) + +def combine_labels(s_sample,segdir, subdir, ls_seg_markers, nuc_diam, cell_diam, df_mi_full,s_thresh): + ''' + - load cell labels; delete cells that were not used for cytoplasm (i.e. ecad neg) + - nuc labels, expand to perinuc 5 and then cut out the cell labels + - keep track of cells that are completely coverd by another cell (or two or three: counts as touching). + ''' + se_neg = df_mi_full[df_mi_full.slide == s_sample].loc[:,f'{s_thresh}_negative'] + dd_result = {} + if os.path.exists(f'{segdir}/{s_sample}Cellpose_Segmentation'): + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation') + else: + os.chdir(segdir) + ls_scene = [] + for s_file in os.listdir(): + if s_file.find(' - DAPI.png') > -1: + ls_scene.append(s_file.split(' - DAPI.png')[0]) + ls_scene = sorted(set(df_mi_full[df_mi_full.slide == s_sample].scene) & set(ls_scene)) + for s_scene in ls_scene: + se_neg_scene = se_neg[se_neg.index.str.contains(s_scene)] + + print(f'Processing combined segmentaiton labels for {s_scene}') + if os.path.exists(f'{s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif'): + labels = io.imread(f'{s_scene} nuclei{nuc_diam} - Nuclei Segmentation Basins.tif') + else: + print('no nuclei labels found') + if os.path.exists(f'{s_scene} matchedcell{cell_diam} - Cell Segmentation Basins.tif'): + cell_labels = io.imread(f'{s_scene} matchedcell{cell_diam} - Cell Segmentation Basins.tif') + elif os.path.exists(f'{s_scene}_{".".join(ls_seg_markers)} matchedcell{cell_diam} - Cell Segmentation Basins.tif'): + cell_labels = io.imread(f'{s_scene}_{".".join(ls_seg_markers)} matchedcell{cell_diam} - Cell Segmentation Basins.tif') + elif os.path.exists(f'{s_scene}_{".".join(ls_seg_markers)} nuc{nuc_diam} matchedcell{cell_diam} - Cell Segmentation Basins.tif'): + cell_labels = io.imread(f'{s_scene}_{".".join(ls_seg_markers)} nuc{nuc_diam} matchedcell{cell_diam} - Cell Segmentation Basins.tif') + else: + print('no cell labels found') + #set non-ecad cell labels to zero + a_zeros = np.array([int(item.split('_cell')[1]) for item in se_neg_scene[se_neg_scene].index]).astype('int64') + mask = np.isin(cell_labels, a_zeros) + cell_labels_copy = cell_labels.copy() + cell_labels_copy[mask] = 0 + #make the nuclei under cells zero + labels_copy = labels.copy() + distance = 5 + perinuc5, labels_exp = features.expand_label(labels,distance=distance) + labels_exp[cell_labels_copy > 0] = 0 + #combine calls and expanded nuclei + combine = (labels_exp + cell_labels_copy) + if s_scene.find('Scene') == 0: + io.imsave(f'{s_sample}_{s_scene.replace("Scene ","scene")}_cell{cell_diam}_nuc{nuc_diam}_CombinedSegmentationBasins.tif',combine) + else: + io.imsave(f'{s_scene}_{".".join(ls_seg_markers)}-cell{cell_diam}_exp{distance}_CellSegmentationBasins.tif',combine) + #figure out the covered cells...labels + combined + not_zero_pixels = np.array([labels.ravel() !=0,combine.ravel() !=0]).all(axis=0) + a_tups = np.array([combine.ravel()[not_zero_pixels],labels.ravel()[not_zero_pixels]]).T #combined over nuclei + unique_rows = np.unique(a_tups, axis=0) + new_dict = {} + for key, value in unique_rows: + if key == value: + continue + else: + if key in new_dict: + new_dict[key].append(value) + else: + new_dict[key] = [value] + #from elmar (reformat cells touching dictionary and save + d_result = {} + for i_cell, li_touch in new_dict.items(): + d_result.update({str(i_cell): [str(i_touch) for i_touch in li_touch]}) + dd_result.update({f'{s_sample}_{s_scene.replace("Scene ","scene")}':d_result}) + #save dd_touch as json file + with open(f'result_{s_sample}_cellsatop_dictionary.json','w') as f: + json.dump(dd_result, f) + print('') + return(labels,combine,dd_result) + +def cropped_ometiff(s_sample,subdir,cropdir,d_crop,d_combos,s_dapi,tu_dim): + if os.path.exists(f'{subdir}/{s_sample}'): + os.chdir(f'{subdir}/{s_sample}') + df_img = parse_org() + df_img['scene'] = s_sample + d_crop_scene = {s_sample:d_crop[s_sample]} + dd_result = mpimage.overlay_crop(d_combos,d_crop_scene,df_img,s_dapi,tu_dim) + for s_crop, d_result in dd_result.items(): + for s_type, (ls_marker, array) in d_result.items(): + print(f'Generating multi-page ome-tiff {[item for item in ls_marker]}') + new_array = array[np.newaxis,np.newaxis,:] + s_xml = ometiff.gen_xml(new_array, ls_marker) + with tifffile.TiffWriter(f'{cropdir}/{s_crop}_{s_type}.ome.tif') as tif: + tif.save(new_array, photometric = "minisblack", description=s_xml, metadata = None) + + +#old +def convert_dapi(debugdir,regdir,b_mkdir=True): + ''' + convert dapi to tif, rename to match Guillaumes pipeline requirements + ''' + cwd = os.getcwd() + os.chdir(debugdir) + for s_dir in sorted(os.listdir()): + if s_dir.find('R-1_')== 0: + os.chdir(s_dir) + for s_file in sorted(os.listdir()): + if s_file.find('bleach')==-1: + s_round = s_file.split("Cycle(")[1].split(").ome.tif")[0] + print(f'stain {s_round}') + s_dir_new = s_dir.split('_')[2] + '-Scene-0' + s_dir.split('F-')[1] + s_tissue_dir = s_dir.split('_F-')[0] + if b_mkdir: + preprocess.cmif_mkdir([f'{regdir}/{s_tissue_dir}']) + a_dapi = skimage.io.imread(s_file) + #rename with standard name (no stain !!!!) + with skimage.external.tifffile.TiffWriter(f'{regdir}/{s_tissue_dir}/{s_dir_new}_R{s_round}_DAPI_V0_c1_ORG_5.0.tif') as tif: + tif.save(a_dapi) + os.chdir('..') + os.chdir(cwd) + +def convert_channels(processdir, regdir, b_rename=True, testbool=True): + ''' + convert channels to tif, select one exposure time of three, rename to match Guillaumes pipeline requirements + ''' + cwd = os.getcwd() + os.chdir(processdir) + for s_dir in sorted(os.listdir()): + if s_dir.find('R-1_')== 0: + os.chdir(s_dir) + if b_rename: + d_rename = {'autofluorescencePE_P':'autofluorescencePE_V0_P', + 'autofluorescenceFITC_F':'autofluorescenceFITC_V0_F', + '000_DAPIi':'extra000_DAPIi', + '000_DAPIf':'extra000_DAPIf', + 'extraextraextra':'extra', + 'extraextra':'extra', + '_FITC_':'_c2_ORG_', + '_PE_':'_c3_ORG_',} + preprocess.dchange_fname(d_rename,b_test=testbool) + + #parse file names + else: + ls_column = ['rounds','marker','dilution','fluor','ORG','exposure','expdecimal','imagetype1','imagetype'] + df_img = mpimage.parse_img(s_end =".tif",s_start='0',s_sep1='_',s_sep2='.',ls_column=ls_column,b_test=False) + df_img['exposure'] = df_img.exposure.astype(dtype='int') + ls_marker = sorted(set(df_img.marker)) + for s_marker in ls_marker: + df_marker = df_img[df_img.marker==s_marker] + df_sort = df_marker.sort_values(by=['exposure'],ascending=False,inplace=False) + for idx in range(len(df_sort.index)): + s_index = df_sort.index[idx] + a_img = skimage.io.imread(s_index) + df_file = df_sort.loc[s_index,:] + print(a_img.max()) + if idx < len(df_sort.index) - 1: + if a_img.max() < 65535: + print(f'Selected {df_file.exposure} for {df_file.marker}') + s_dir_new = s_dir.split('_')[2] + '-Scene-0' + s_dir.split('F-')[1] + s_tissue_dir = s_dir.split('_F-')[0] + s_index_new = s_index.split(".ome.tif")[0] + with skimage.external.tifffile.TiffWriter(f'{regdir}/{s_tissue_dir}/{s_dir_new}_R{s_index_new}.tif') as tif: + tif.save(a_img) + break + else: + print('Try lower exposure time') + elif idx == len(df_sort.index) - 1: + print(f'Selected as the lowest exposure time {df_file.exposure} for {df_file.marker}') + s_dir_new = s_dir.split('_')[2] + '-Scene-0' + s_dir.split('F-')[1] + s_tissue_dir = s_dir.split('_F-')[0] + s_index_new = s_index.split(".ome.tif")[0] + with skimage.external.tifffile.TiffWriter(f'{regdir}/{s_tissue_dir}/{s_dir_new}_R{s_index_new}.tif') as tif: + tif.save(a_img) + else: + print('/n /n /n /n Error in finding exposure time') + + os.chdir('..') + +def parse_converted(regdir): + ''' + parse the converted miltenyi file names, + regdir contains the images + ''' + s_dir = os.getcwd() + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='G',s_split='_') + df_img.rename({'data':'scene'},axis=1,inplace=True) + df_img['rounds'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[2] for item in [item.split('_') for item in df_img.index]] + df_img['dilution'] = [item[3] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[4] for item in [item.split('_') for item in df_img.index]] + df_img['scene_int'] = [item.split('Scene-')[1] for item in df_img.scene] + df_img['scene_int'] = df_img.scene_int.astype(dtype='int') + df_img['exposure'] = [item[6].split('.')[0] for item in [item.split('_') for item in df_img.index]] + df_img['path'] = [f'{regdir}/{s_dir}/{item}' for item in df_img.index] + df_img['tissue'] = s_dir + return(df_img) + +def parse_converted_dirs(regdir): + ''' + parse the converted miltenyi file names, + regdir is the master folder containing subfolders with ROIs/gates + ''' + os.chdir(regdir) + df_img_all = pd.DataFrame() + for idx, s_dir in enumerate(sorted(os.listdir())): + os.chdir(s_dir) + s_sample = s_dir + print(s_sample) + df_img = parse_converted(s_dir) + df_img_all = df_img_all.append(df_img) + os.chdir('..') + return(df_img_all) + +def count_images(df_img,b_tile_count=True): + """ + count and list slides, scenes, rounds + """ + df_count = pd.DataFrame(index=sorted(set(df_img.scene)),columns=sorted(set(df_img.color))) + for s_sample in sorted(set(df_img.tissue)): + print(f'ROI {s_sample}') + df_img_slide = df_img[df_img.tissue==s_sample] + print('tiles') + [print(item) for item in sorted(set(df_img_slide.scene))] + print(f'Number of images = {len(df_img_slide)}') + print(f'Rounds:') + [print(item) for item in sorted(set(df_img_slide.rounds))] + print('\n') + if b_tile_count: + for s_scene in sorted(set(df_img_slide.scene)): + df_img_scene = df_img_slide[df_img_slide.scene==s_scene] + for s_color in sorted(set(df_img_scene.color)): + print(f'{s_scene} {s_color} {len(df_img_scene[df_img_scene.color==s_color])}') + df_count.loc[s_scene,s_color] = len(df_img_scene[df_img_scene.color==s_color]) + return(df_count) + +def visualize_reg_images(regdir,qcdir,color='c1',tu_array=(3,2)): + """ + array registered images to check tissue identity, focus, etc. + """ + #check registration + preprocess.cmif_mkdir([f'{qcdir}/RegisteredImages']) + cwd = os.getcwd() + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + os.chdir(s_dir) + s_sample = s_dir + print(s_sample) + df_img = parse_converted(s_dir) + ls_scene = sorted(set(df_img.scene)) + for s_scene in ls_scene: + print(s_scene) + df_img_scene = df_img[df_img.scene == s_scene] + df_img_stain = df_img_scene[df_img_scene.color==color] + df_img_sort = df_img_stain.sort_values(['rounds']) + i_sqrt = math.ceil(math.sqrt(len(df_img_sort))) + #array_img(df_img,s_xlabel='color',ls_ylabel=['rounds','exposure'],s_title='marker',tu_array=(2,4),tu_fig=(10,20)) + if color == 'c1': + fig = mpimage.array_img(df_img_sort,s_xlabel='marker',ls_ylabel=['rounds','exposure'],s_title='rounds',tu_array=tu_array,tu_fig=(16,14)) + else: + fig = mpimage.array_img(df_img_sort,s_xlabel='color',ls_ylabel=['rounds','exposure'],s_title='marker',tu_array=tu_array,tu_fig=(16,12)) + fig.savefig(f'{qcdir}/RegisteredImages/{s_scene}_registered_{color}.png') + os.chdir('..') + os.chdir(cwd) + #return(df_img) + +def rename_files(d_rename,dir,b_test=True): + """ + change file names + """ + cwd = os.getcwd() + os.chdir(dir) + for idx, s_dir in enumerate(sorted(os.listdir())): + s_path = f'{dir}/{s_dir}' + os.chdir(s_path) + print(s_dir) + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='reg',s_split='_') + df_img.rename({'data':'scene'},axis=1,inplace=True) + df_img['rounds'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[2] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[3].split('.')[0] for item in [item.split('_') for item in df_img.index]] + if b_test: + print('This is a test') + preprocess.dchange_fname(d_rename,b_test=True) + elif b_test==False: + print('Changing name - not a test') + preprocess.dchange_fname(d_rename,b_test=False) + else: + pass + +def rename_fileorder(s_sample, dir, b_test=True): + """ + change file names + """ + cwd = os.getcwd() + os.chdir(dir) + for idx, s_dir in enumerate(sorted(os.listdir())): + s_path = f'{dir}/{s_dir}' + os.chdir(s_path) + print(s_dir) + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='Scene',s_split='_') + df_img.rename({'data':'scene'},axis=1,inplace=True) + df_img['rounds'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[2] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[3].split('.')[0] for item in [item.split('_') for item in df_img.index]] + for s_index in df_img.index: + s_round = df_img.loc[s_index,'rounds'] + s_scene= f"{s_sample}-{df_img.loc[s_index,'scene']}" + s_marker = df_img.loc[s_index,'marker'] + s_color = df_img.loc[s_index,'color'] + s_index_rename = f'{s_round}_{s_scene}_{s_marker}_{s_color}_ORG.tif' + d_rename = {s_index:s_index_rename} + if b_test: + print('This is a test') + preprocess.dchange_fname(d_rename,b_test=True) + elif b_test==False: + print('Changing name - not a test') + preprocess.dchange_fname(d_rename,b_test=False) + else: + pass + + +def copy_files(dir,dapi_copy, marker_copy,testbool=True,type='codex'): + """ + copy and rename files if needed as dummies + need to edit + """ + os.chdir(dir) + for idx, s_dir in enumerate(sorted(os.listdir())): + s_path = f'{dir}/{s_dir}' + os.chdir(s_path) + #s_sample = s_dir.split('-Scene')[0] + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='Scene',s_split='_') + df_img.rename({'data':'scene'},axis=1,inplace=True) + df_img['rounds'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[2] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[3].split('.')[0] for item in [item.split('_') for item in df_img.index]] + print(s_dir) + #if b_test: + for key, dapi_item in dapi_copy.items(): + df_dapi = df_img[(df_img.rounds== key.split('_')[1]) & (df_img.color=='c1')] + s_dapi = df_dapi.loc[:,'marker'][0] + preprocess.copy_dapis(s_r_old=key,s_r_new=f'_cyc{dapi_item}_',s_c_old='_c1_', + s_c_new='_c2_',s_find=f'_c1_{s_dapi}_ORG.tif',b_test=testbool,type=type) + i_count=0 + for idx,(key, item) in enumerate(marker_copy.items()): + preprocess.copy_markers(df_img, s_original=key, ls_copy = item, + i_last_round= dapi_item + i_count, b_test=testbool,type=type) + i_count=i_count + len(item) + +def segmentation_thresholds(regdir,qcdir, d_segment): + """ + visualize binary mask of segmentaiton threholds + need to edit + """ + preprocess.cmif_mkdir([f'{qcdir}/Segmentation']) + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + s_path = f'{regdir}/{s_dir}' + os.chdir(s_path) + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='Scene',s_split='_') + df_img.rename({'data':'scene'},axis=1,inplace=True) + df_img['rounds'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[2] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[3].split('.')[0] for item in [item.split('_') for item in df_img.index]] + s_sample = s_dir + print(s_sample) + d_seg = preprocess.check_seg_markers(df_img,d_segment, i_rows=1, t_figsize=(6,6)) #few scenes + for key, fig in d_seg.items(): + fig.savefig(f'{qcdir}/Segmentation/{s_dir}_{key}_segmentation.png') + + +def segmentation_inputs(s_sample,regdir,segdir,d_segment,b_start=False): + """ + make inputs for guillaumes segmentation + """ + os.chdir(regdir) + for idx, s_dir in enumerate(sorted(os.listdir())): + s_path = f'{regdir}/{s_dir}' + os.chdir(s_path) + df_img = mpimage.filename_dataframe(s_end = ".tif",s_start='R',s_split='_') + df_img.rename({'data':'rounds'},axis=1,inplace=True) + #df_img['rounds'] = [item[1] for item in [item.split('_') for item in df_img.index]] + df_img['color'] = [item[3] for item in [item.split('_') for item in df_img.index]] + df_img['marker'] = [item[2] for item in [item.split('_') for item in df_img.index]] + #s_sample = s_dir + #s_sample = s_dir.split('-Scene')[0] + print(s_sample) + df_marker = df_img[df_img.color!='c1'] + df_marker = df_marker.sort_values(['rounds','color']) + df_dapi = pd.DataFrame(index = [df_marker.marker.tolist()],columns=['rounds','colors','minimum','maximum','exposure','refexp','location']) + df_dapi['rounds'] = df_marker.loc[:,['rounds']].values + df_dapi['colors'] = df_marker.loc[:,['color']].values + df_dapi['minimum'] = 1003 + df_dapi['maximum'] = 65535 + df_dapi['exposure'] = 100 + df_dapi['refexp'] = 100 + df_dapi['location'] = 'All' + for s_key,i_item in d_segment.items(): + df_dapi.loc[s_key,'minimum'] = i_item + df_dapi.to_csv('RoundsCyclesTable.txt',sep=' ',header=False) + df_dapi.to_csv(f'metadata_{s_sample}_RoundsCyclesTable.csv',header=True) + #create cluster.java file + preprocess.cluster_java(s_dir=f'JE{idx}',s_sample=s_sample,imagedir=f'{s_path}',segmentdir=segdir,type='exacloud',b_segment=True,b_TMA=False) + if b_start: + os.chdir(f'/home/groups/graylab_share/Chin_Lab/ChinData/Work/engje/exacloud/JE{idx}') #exacloud + print(f'JE{idx}') + os.system('make_sh') diff --git a/mplex_image/mpimage.py b/mplex_image/mpimage.py new file mode 100755 index 0000000..86746e4 --- /dev/null +++ b/mplex_image/mpimage.py @@ -0,0 +1,817 @@ +#### +# title: mpimage.py +# +# language: Python3.6 +# date: 2019-05-00 +# license: GPL>=v3 +# author: Jenny +# +# description: +# python3 library to display, normalize and crop multiplex images +#### + +#libraries +import matplotlib as mpl +mpl.use('agg') +import matplotlib.pyplot as plt +import numpy as np +import os +import skimage +import pandas as pd +#import bioformats +import re +import shutil +from itertools import chain +import matplotlib.ticker as ticker + +#os.chdir('/home/groups/graylab_share/OMERO.rdsStore/engje/Data/cmIF/') +#from apeer_ometiff_library import omexmlClass + +#functions + + +def parse_img(s_end =".tif",s_start='',s_sep1='_',s_sep2='.',s_exclude='Gandalf',ls_column=['rounds','color','imagetype','scene'],b_test=True): + ''' + required columns: ['rounds','color','imagetype','scene'] + meta names names=['rounds','color','minimum', 'maximum', 'exposure', 'refexp','location'],#'marker', + return = df_img + ''' + ls_file = [] + for file in os.listdir(): + #find all filenames ending in s_end + if file.endswith(s_end): + if file.find(s_start)==0: + if file.find(s_exclude)==-1: + ls_file = ls_file + [file] + + print(f'test {int(1.1)}') + #make a list of list of file name items separated by s_sep + llls_split = [] + for items in [item.split(s_sep1)for item in ls_file]: + llls_split.append([item.split(s_sep2) for item in items]) + + lls_final = [] + for lls_split in llls_split: + lls_final.append(list(chain.from_iterable(lls_split))) + + #make a blank dataframe with the index being the filename + df_img = pd.DataFrame(index=ls_file, columns=ls_column) + if b_test: + print(lls_final[0]) + print(f'Length = {len(lls_final[0])}') + #add a column for each part of the name + else: + for fidx, ls_final in enumerate(lls_final): + for idx, s_name in enumerate(ls_final): + df_img.loc[ls_file[fidx],ls_column[idx]] = s_name + print('Mean number of items in file name') + print(np.asarray([(len(item)) for item in lls_final]).mean()) + if (np.asarray([(len(item)) for item in lls_final]).mean()).is_integer()==False: + print([(len(item)) for item in lls_final]) + i_right = np.asarray([(len(item)) for item in lls_final]).max() + for fidx, ls_final in enumerate(lls_final): + if len(ls_final) < i_right: + print(f' inconsitent name: {ls_file[fidx]}') + return(df_img) + +def parse_org(s_end = "ORG.tif",s_start='R',type='reg'): + """ + This function will parse images following koei's naming convention + Example: Registered-R1_PCNA.CD8.PD1.CK19_Her2B-K157-Scene-002_c1_ORG.tif + The output is a dataframe with image filename in index + And rounds, color, imagetype, scene (/tissue), and marker in the columns + type= 'reg' or 'raw' + """ + + ls_file = [] + for file in os.listdir(): + #find all filenames ending in s_end + if file.endswith(s_end): + if file.find(s_start)==0: + ls_file = ls_file + [file] + lls_name = [item.split('_') for item in ls_file] + df_img = pd.DataFrame(index=ls_file) + if type == 'raw': + lls_scene = [item.split('-Scene-') for item in ls_file] + elif type== 'noscenes': + ls_scene = ['Scene-001'] * len(ls_file) + if type == 'raw': + df_img['rounds'] = [item[0] for item in lls_name] + elif type== 'noscenes': + df_img['rounds'] = [item[0] for item in lls_name] + else: + df_img['rounds'] = [item[0].split('Registered-')[1] for item in lls_name] + df_img['color'] = [item[-2] for item in lls_name] + df_img['imagetype'] = [item[-1].split('.tif')[0] for item in lls_name] + if type == 'raw': + df_img['slide'] = [item[2] for item in lls_name] + try: + df_img['scene'] = [item[1].split('_')[0] for item in lls_scene] + except IndexError: + print(f"{set([item[0] for item in lls_scene])}") + elif type == 'noscenes': + df_img['slide'] = [item[2] for item in lls_name] + df_img['scene'] = ls_scene + else: + df_img['scene'] = [item[2] for item in lls_name] + df_img['round_ord'] = [re.sub('Q','.5', item) for item in df_img.rounds] + df_img['round_ord'] = [float(re.sub('[^0-9.]','', item)) for item in df_img.round_ord] + df_img = df_img.sort_values(['round_ord','rounds','color']) + for idx, s_round in enumerate(df_img.rounds.unique()): + df_img.loc[df_img.rounds==s_round, 'round_num'] = idx + #parse file name for biomarker + for s_index in df_img.index: + #print(s_index) + s_color = df_img.loc[s_index,'color'] + if s_color == 'c1': + s_marker = 'DAPI' + elif s_color == 'c2': + s_marker = s_index.split('_')[1].split('.')[0] + elif s_color == 'c3': + s_marker = s_index.split('_')[1].split('.')[1] + elif s_color == 'c4': + s_marker = s_index.split('_')[1].split('.')[2] + elif s_color == 'c5': + s_marker = s_index.split('_')[1].split('.')[3] + #these are only included in sardana shading corrected images + elif s_color == 'c6': + s_marker = s_index.split('_')[1].split('.')[2] + elif s_color == 'c7': + s_marker = s_index.split('_')[1].split('.')[3] + else: print('Error') + df_img.loc[s_index,'marker'] = s_marker + + return(df_img) #,lls_name) + +def filename_dataframe(s_end = ".czi",s_start='R',s_split='_'): + ''' + quick and dirty way to select files for dataframe. + s_end = string at end of file names + s_start = string at beginning of filenames + s_split = character/string in all file names + ''' + ls_file = [] + for file in os.listdir(): + #find all filenames ending in 'ORG.tif' + if file.endswith(s_end): + if file.find(s_start)==0: + ls_file = ls_file + [file] + lls_name = [item.split(s_split) for item in ls_file] + df_img = pd.DataFrame(index=ls_file) + df_img['data'] = [item[0] for item in lls_name] + return(df_img) + +def underscore_to_dot(s_sample, s_end='ORG.tif', s_start='R',s_split='_'): + df = filename_dataframe(s_end,s_start,s_split) + ls_old = sorted(set([item.split(f'_{s_sample}')[0] for item in df.index])) + ls_new = sorted(set([item.split(f'_{s_sample}')[0].replace('_','.').replace(f"{df.loc[item,'data']}.",f"{df.loc[item,'data']}_") for item in df.index])) + d_replace = dict(zip(ls_old,ls_new)) + for key, item in d_replace.items(): + if key.split('_')[0] != item.split('_')[0]: + print(f' Error {key} mathced to {item}') + return(d_replace) + +def add_exposure(df_img,df_t,type='roundcycles'): + """ + df_img = dataframe of images with columns [ 'color', 'exposure', 'marker','sub_image','sub_exposure'] + and index with image names + df_t = metadata with dataframe with ['marker','exposure'] + """ + if type == 'roundscycles': + for s_index in df_img.index: + s_marker = df_img.loc[s_index,'marker'] + #look up exposure time for marker in metadata + df_t_image = df_t[(df_t.marker==s_marker)] + if len(df_t_image) > 0: + i_exposure = df_t_image.iloc[0].loc['exposure'] + df_img.loc[s_index,'exposure'] = i_exposure + else: + print(f'{s_marker} has no recorded exposure time') + elif type == 'czi': + #add exposure + df_t['rounds'] = [item.split('_')[0] for item in df_t.index] + #df_t['tissue'] = [item.split('_')[2].split('-Scene')[0] for item in df_t.index] #not cool with stiched + for s_index in df_img.index: + s_tissue = df_img.loc[s_index,'scene'].split('-Scene')[0] + s_color = str(int(df_img.loc[s_index,'color'].split('c')[1])-1) + s_round = df_img.loc[s_index,'rounds'] + print(s_index) + df_img.loc[s_index,'exposure'] = df_t[(df_t.index.str.contains(s_tissue)) & (df_t.rounds==s_round)].loc[:,s_color][0] + + return(df_img) + +def subtract_images(df_img,d_channel={'c2':'L488','c3':'L555','c4':'L647','c5':'L750'},ls_exclude=[],subdir='SubtractedRegisteredImages',b_8bit=True):#b_mkdir=True, + """ + This code loads 16 bit grayscale tiffs, performs AF subtraction of channels/rounds defined by the user, and outputs 8 bit AF subtracted tiffs for visualization. + The data required is: + 1. The RoundsCyclesTable with real exposure times + 2. dataframe of images to process (df_img); can be created with any custom parsing function + df_img = dataframe of images with columns [ 'color', 'exposure', 'marker'] + and index with image names + d_channel = dictionary mapping color to marker to subtract + ls_exclude = lost of markers not needing subtraction + """ + #generate dataframe of subtraction markers + es_subtract = set() + for s_key, s_value in d_channel.items(): + es_subtract.add(s_value) + print(f'Subtracting {s_value} for all {s_key}') + + df_subtract = pd.DataFrame() + for s_subtract in sorted(es_subtract): + se_subtract = df_img[df_img.marker==s_subtract] + df_subtract = df_subtract.append(se_subtract) + print(f'The background images {df_subtract.index.tolist}') + print(f'The background markers {df_subtract.marker.tolist}') + + #generate dataframe of how subtraction is set up + #set of markers minus the subtraction markers + es_markers = set(df_img.marker) - es_subtract + #dataframe of markers + df_markers = df_img[df_img.loc[:,'marker'].isin(sorted(es_markers))] + #minus dapi (color 1 or DAPI) + #df_markers = df_markers[df_markers.loc[:,'color']!='c1'] + #df_markers = df_markers[~df_markers.loc[:,'marker'].str.contains('DAPI')] + df_copy = df_img[df_img.marker.isin(ls_exclude)] + df_markers = df_markers[~df_markers.marker.isin(ls_exclude)] + + for s_file in df_copy.index.tolist(): + print(s_file) + #print(f'copied to ./AFSubtracted/{s_file}') + #shutil.copyfile(s_file,f'./AFSubtracted/{s_file}') + print(f'copied to {subdir}/{s_file}') + shutil.copyfile(s_file,f'{subdir}/{s_file}') + #ls_scene = sorted(set(df_img.scene)) + #add columns with mapping of proper subtracted image to dataframe + + for s_index in df_markers.index.tolist(): + print('add colums') + print(s_index) + s_scene = s_index.split('_')[2] + s_color = df_markers.loc[s_index,'color'] + if len(df_subtract[(df_subtract.color==s_color) & (df_subtract.scene==s_scene)])==0: + print(f'missing {s_color} in {s_scene}') + else: + df_markers.loc[s_index,'sub_image'] = df_subtract[(df_subtract.color==s_color) & (df_subtract.scene==s_scene)].index[0] + df_markers.loc[s_index,'sub_exposure'] = df_subtract[(df_subtract.color==s_color) & (df_subtract.scene==s_scene)].exposure[0] + + #loop to subtract + for s_index in df_markers.index.tolist(): + print(f'Processing {s_index}') + s_image = s_index + s_color = '_' + df_markers.loc[s_index,'color'] + '_' + s_background = df_markers.loc[s_index,'sub_image'] + print(f'From {s_image} subtracting \n {s_background}') + a_img = skimage.io.imread(s_image) + a_AF = skimage.io.imread(s_background) + #divide each image by exposure time + #subtract 1 ms AF from 1 ms signal + #multiply by original image exposure time + a_sub = (a_img/df_markers.loc[s_index,'exposure'] - a_AF/df_markers.loc[s_index,'sub_exposure'])*df_markers.loc[s_index,'exposure'] + a_zero = (a_sub.clip(min=0)).astype(int) #max=a_sub.max() #took out max parameter from np.clip, but it was fine in + if b_8bit: + #a_16bit = skimage.img_as_ubyte(a_zero) + #a_zero = a_sub.clip(min=0,max=a_sub.max()) + a_bit = (a_zero/256).astype(np.uint8) + else: + a_bit = skimage.img_as_uint(a_zero) + s_fname = f'{subdir}/{s_index.split(s_color)[0]}_Sub{df_subtract.loc[df_markers.loc[s_index,"sub_image"],"marker"]}{s_color}{s_index.split(s_color)[1]}' + skimage.io.imsave(s_fname,a_bit) + + return(df_markers,df_copy)#df_markers,es_subtract + +def subtract_scaled_images(df_img,d_late={'c2':'R5Qc2','c3':'R5Qc3','c4':'R5Qc4','c5':'R5Qc5'},d_early={'c2':'R0c2','c3':'R0c3','c4':'R0c4','c5':'R0c5'},ls_exclude=[],subdir='SubtractedRegisteredImages',b_8bit=False): + """ + This code loads 16 bit grayscale tiffs, performs scaled AF subtraction + based on the round position between early and late AF channels/rounds defined by the user, + and outputs AF subtracted tiffs or ome-tiffs for visualization. + The data required is: + 1. The RoundsCyclesTable with real exposure times + 2. dataframe of images to process (df_img); can be created with any custom parsing function + df_img = dataframe of images with columns [ 'color', 'exposure', 'marker','round_ord'] + and index with image names + d_channel = dictionary mapping color to marker to subtract + ls_exclude = lost of markers not needing subtraction + """ + #generate dataframe of subtraction markers + es_subtract = set() + [es_subtract.add(item) for key, item in d_early.items()] + [es_subtract.add(item) for key, item in d_late.items()] + + #markers minus the subtraction markers & excluded markers + es_markers = set(df_img.marker) - es_subtract + #dataframe of markers + df_markers = df_img[df_img.loc[:,'marker'].isin(es_markers)] + df_copy = df_img[df_img.marker.isin(ls_exclude)] + df_markers = df_markers[~df_markers.marker.isin(ls_exclude)] + + #copy excluded markers + for s_file in df_copy.index.tolist(): + print(s_file) + print(f'copied to {subdir}/{s_file}') + shutil.copyfile(s_file,f'{subdir}/{s_file}') + + #add columns with mapping of proper AF images to marker images + for s_index in df_markers.index.tolist(): + print('add colums') + print(s_index) + s_scene = df_markers.loc[s_index,'scene'] + s_color = df_markers.loc[s_index,'color'] + s_early = d_early[s_color] + s_late = d_late[s_color] + i_round = df_markers.loc[s_index,'round_num'] + df_scene = df_img[df_img.scene==s_scene] + if len(df_scene[df_scene.marker==s_early]) == 0: + print(f' Missing early AF channel for {s_scene} {s_color}') + elif len(df_scene[df_scene.marker==s_late]) == 0: + print(f' Missing late AF channel for {s_scene} {s_color}') + else: + i_early = df_scene[(df_scene.marker==s_early)].round_num[0] + i_late = df_scene[(df_scene.marker==s_late)].round_num[0] + df_markers.loc[s_index,'sub_name'] = f'{s_early}{s_late}' + df_markers.loc[s_index,'sub_early'] = df_scene[(df_scene.marker==s_early)].index[0] + df_markers.loc[s_index,'sub_early_exp'] = df_scene[(df_scene.marker==s_early)].exposure[0] + df_markers.loc[s_index,'sub_late'] = df_scene[(df_scene.marker==s_late)].index[0] + df_markers.loc[s_index,'sub_late_exp'] = df_scene[(df_scene.marker==s_late)].exposure[0] + df_markers.loc[s_index,'sub_ratio_late'] = np.clip((i_round-i_early)/(i_late - i_early),0,1) + df_markers.loc[s_index,'sub_ratio_early'] = np.clip(1 - (i_round-i_early)/(i_late - i_early),0,1) + + #loop to subtract + for s_index in df_markers.index.tolist(): + print(f'Processing {s_index}') + s_color = '_' + df_markers.loc[s_index,'color'] + '_' + a_img = skimage.io.imread(s_index) + a_early = skimage.io.imread(df_markers.loc[s_index,'sub_early']) + a_late = skimage.io.imread(df_markers.loc[s_index,'sub_late']) + #divide each image by exposure time + a_img_exp = a_img/df_markers.loc[s_index,'exposure'] + a_early_exp = a_early/df_markers.loc[s_index,'sub_early_exp'] + a_late_exp = a_late/df_markers.loc[s_index,'sub_late_exp'] + #combine early and late based on round_num + a_early_exp = a_early_exp * df_markers.loc[s_index,'sub_ratio_early'] + a_late_exp = a_late_exp * df_markers.loc[s_index,'sub_ratio_late'] + #subtract 1 ms AF from 1 ms signal + #multiply by original image exposure time + a_sub = (a_img_exp - a_early_exp - a_late_exp)*df_markers.loc[s_index,'exposure'] + a_zero = (a_sub.clip(min=0)).astype(int) # + if b_8bit: + a_bit = (a_zero/256).astype(np.uint8) + else: + a_bit = skimage.img_as_uint(a_zero) + s_fname = f'{subdir}/{s_index.split(s_color)[0]}_Sub{df_markers.loc[s_index,"sub_name"]}{s_color}{s_index.split(s_color)[1]}' + skimage.io.imsave(s_fname,a_bit) + + return(df_markers,df_copy) + +def overlay_crop(d_combos,d_crop,df_img,s_dapi,tu_dim=(1000,1000),b_8bit=True): + """ + output custon multi page tiffs according to dictionary, with s_dapi as channel 1 in each overlay + BUG with 53BP1 + d_crop : {slide_scene : (x,y) coord + tu_dim = (width, height) + d_combos = {'Immune':{'CD45', 'PD1', 'CD8', 'CD4', 'CD68', 'FoxP3','GRNZB','CD20','CD3'}, + 'Stromal':{'Vim', 'aSMA', 'PDPN', 'CD31', 'ColIV','ColI'}, + 'Differentiation':{'CK19', 'CK7','CK5', 'CK14', 'CK17','CK8'}, + 'Tumor':{'HER2', 'Ecad', 'ER', 'PgR','Ki67','PCNA'}, + 'Proliferation':{'EGFR','CD44','AR','pHH3','pRB'}, + 'Functional':{'pS6RP','H3K27','H3K4','cPARP','gH2AX','pAKT','pERK'}, + 'Lamins':{'LamB1','LamAC', 'LamB2'}} + """ + dd_result = {} + for s_index in df_img.index: + s_marker = df_img.loc[s_index,'marker'] + if s_marker == 'DAPI': + s_marker = s_marker + f'{df_img.loc[s_index,"rounds"].split("R")[1]}' + df_img.loc[s_index,'marker'] = s_marker + #now make overlays + for s_scene, xy_cropcoor in d_crop.items(): + d_result = {} + print(f'Processing {s_scene}') + df_slide = df_img[df_img.scene==s_scene] + s_image_round = df_slide[df_slide.marker==s_dapi].index[0] + if len(df_slide[df_slide.marker==s_dapi.split('_')[0]].index) == 0: + print('Error: dapi not found') + elif len(df_slide[df_slide.marker==s_dapi.split('_')[0]].index) > 1: + print('Error: too many dapi images found') + else: + print(s_image_round) + #exclude any missing biomarkers + es_all = set(df_slide.marker) + #iterate over overlay combinations + for s_type, es_combos in d_combos.items(): + d_overlay = {} + es_combos_shared = es_combos.intersection(es_all) + for idx, s_combo in enumerate(sorted(es_combos_shared)): + s_filename = (df_slide[df_slide.marker==s_combo]).index[0] + if len((df_slide[df_slide.marker==s_combo]).index) == 0: + print(f'Error: {s_combo} not found') + elif len((df_slide[df_slide.marker==s_combo]).index) > 1: + print(f'\n Warning {s_combo}: too many marker images found, used {s_filename}') + else: + print(f'{s_combo}: {s_filename}') + d_overlay.update({s_combo:s_filename}) + #d_overlay.update({s_dapi:s_image_round}) + a_dapi = skimage.io.imread(s_image_round) + #crop + a_crop = a_dapi[(xy_cropcoor[1]):(xy_cropcoor[1]+tu_dim[1]),(xy_cropcoor[0]):(xy_cropcoor[0]+tu_dim[0])] + a_overlay = np.zeros((len(d_overlay) + 1,a_crop.shape[0],a_crop.shape[1]),dtype=np.uint8) + if a_crop.dtype == 'uint16': + if b_8bit: + a_crop = (a_crop/256).astype(np.uint8) + else: + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range=(0,1.5*np.quantile(a_crop,0.9999))) + a_crop = (a_rescale/256).astype(np.uint8) + print(f'rescale intensity') + a_overlay[0,:,:] = a_crop + ls_biomarker_all = [s_dapi] + for i, s_color in enumerate(sorted(d_overlay.keys())): + s_overlay= d_overlay[s_color] + ls_biomarker_all.append(s_color) + a_channel = skimage.io.imread(s_overlay) + #crop + a_crop = a_channel[(xy_cropcoor[1]):(xy_cropcoor[1]+tu_dim[1]),(xy_cropcoor[0]):(xy_cropcoor[0]+tu_dim[0])] + if a_crop.dtype == 'uint16': + if b_8bit: + a_crop = (a_crop/256).astype(np.uint8) + else: + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range=(0,1.5*np.quantile(a_crop,0.9999))) + a_crop = (a_rescale/256).astype(np.uint8) + print(f'rescale intensity') + a_overlay[i + 1,:,:] = a_crop + d_result.update({s_type:(ls_biomarker_all,a_overlay)}) + dd_result.update({f'{s_scene}_x{xy_cropcoor[0]}y{xy_cropcoor[1]}':d_result}) + return(dd_result) + +def gen_xml(array, channel_names): + ''' + copy and modify from apeer ome tiff + ls_marker + ''' + #for idx, s_marker in enumerate(ls_marker): + # old = bytes(f'Name="C:{idx}"','utf-8') + # new = bytes(f'Name="{s_marker}"','utf-8') + # s_xml = s_xml.replace(old,new,-1) + #Dimension order is assumed to be TZCYX + dim_order = "TZCYX" + + metadata = omexmlClass.OMEXML() + shape = array.shape + assert ( len(shape) == 5), "Expected array of 5 dimensions" + + metadata.image().set_Name("IMAGE") + metadata.image().set_ID("0") + + pixels = metadata.image().Pixels + pixels.ome_uuid = metadata.uuidStr + pixels.set_ID("0") + + pixels.channel_count = shape[2] + + pixels.set_SizeT(shape[0]) + pixels.set_SizeZ(shape[1]) + pixels.set_SizeC(shape[2]) + pixels.set_SizeY(shape[3]) + pixels.set_SizeX(shape[4]) + + pixels.set_DimensionOrder(dim_order[::-1]) + + pixels.set_PixelType(omexmlClass.get_pixel_type(array.dtype)) + + for i in range(pixels.SizeC): + pixels.Channel(i).set_ID("Channel:0:" + str(i)) + pixels.Channel(i).set_Name(channel_names[i]) + + for i in range(pixels.SizeC): + pixels.Channel(i).set_SamplesPerPixel(1) + + pixels.populate_TiffData() + + return metadata.to_xml().encode() + +def array_img(df_img,s_xlabel='color',ls_ylabel=['rounds','exposure'],s_title='marker',tu_array=(2,4),tu_fig=(10,20),cmap='gray',d_crop={}): + """ + create a grid of images + df_img = dataframe of images with columns having image attributes + and index with image names + s_xlabel = coumns of grid + ls_ylabel = y label + s_title= title + + """ + + fig, ax = plt.subplots(tu_array[0],tu_array[1],figsize=tu_fig) + ax = ax.ravel() + for ax_num, s_index in enumerate(df_img.index): + s_row_label = f'{df_img.loc[s_index,ls_ylabel[0]]}\n {df_img.loc[s_index,ls_ylabel[1]]}' + s_col_label = df_img.loc[s_index,s_xlabel] + a_image=skimage.io.imread(s_index) + s_label_img = df_img.loc[s_index,s_title] + a_rescale = skimage.exposure.rescale_intensity(a_image,in_range=(0,1.5*np.quantile(a_image,0.98))) + if len(d_crop)!= 0: + tu_crop = d_crop[df_img.loc[s_index,'scene']] + a_rescale = a_rescale[(tu_crop[1]):(tu_crop[1]+tu_crop[3]),(tu_crop[0]):(tu_crop[0]+tu_crop[2])] + ax[ax_num].imshow(a_rescale,cmap=cmap) + ax[ax_num].set_title(s_label_img) + ax[ax_num].set_ylabel(s_row_label) + ax[ax_num].set_xlabel(f'{s_col_label}\n 0 - {int(1.5*np.quantile(a_image,0.98))}') + plt.tight_layout() + return(fig) + +def array_roi(df_img,s_column='color',s_row='rounds',s_label='marker',tu_crop=(0,0,100,100),tu_array=(2,4),tu_fig=(10,20), cmap='gray',b_min_label=True,tu_rescale=(0,0)): + """ + create a grid of images + df_img = dataframe of images with columns having image attributes + and index with image names + s_column = coumns of grid + s_row = rows of grid + s_label= attribute to label axes + tu_crop = (upper left corner x, y , xlength, yheight) + tu_dim = a tumple of x and y dimensinons of crop + """ + + fig, ax = plt.subplots(tu_array[0],tu_array[1],figsize=tu_fig,sharex=True, sharey=True) + if b_min_label: + fig, ax = plt.subplots(tu_array[0],tu_array[1],figsize=tu_fig, sharey=True) + ax = ax.ravel() + for ax_num, s_index in enumerate(df_img.index): + s_row_label = df_img.loc[s_index,s_row] + s_col_label = df_img.loc[s_index,s_column] + s_label_img = df_img.loc[s_index,s_label] + #load image, copr, rescale + a_image=skimage.io.imread(s_index) + a_crop = a_image[(tu_crop[1]):(tu_crop[1]+tu_crop[3]),(tu_crop[0]):(tu_crop[0]+tu_crop[2])] + if tu_rescale==(0,0): + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range=(0,np.quantile(a_image,0.98)+np.quantile(a_image,0.98)/2)) + tu_max = (0,np.quantile(a_image,0.98)+np.quantile(a_image,0.98)/2) + ax[ax_num].imshow(a_rescale,cmap='gray') + else: + print(f'original {a_crop.min()},{a_crop.max()}') + print(f'rescale to {tu_rescale}') + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range=tu_rescale,out_range=tu_rescale) + tu_max=tu_rescale + ax[ax_num].imshow(a_rescale,cmap=cmap,vmin=0, vmax=tu_max[1]) + ax[ax_num].set_title(s_label_img) + ax[ax_num].set_ylabel(s_row_label) + ax[ax_num].set_xlabel(s_col_label) + if b_min_label: + ax[ax_num].set_xticklabels('') + ax[ax_num].set_xlabel(f'{tu_max[0]} - {int(tu_max[1])}') #min/max = + plt.tight_layout() + return(fig) + +def load_labels(d_crop,segdir,s_find='Nuclei Segmentation Basins'): + """ + load the segmentation basins (cell of nuceli) + s_find: 'exp5_CellSegmentationBasins' or 'Nuclei Segmentation Basins' + """ + d_label={} + cwd = os.getcwd() + for s_scene, xy_cropcoor in d_crop.items(): + print(s_scene) + s_sample = s_scene.split('-Scene-')[0] + os.chdir(f'{segdir}') + for s_file in os.listdir(): + if s_file.find(s_find) > -1: #Nuclei Segmentation Basins.tif #Cell Segmentation Basins.tif + if s_file.find(s_scene.split(s_sample)[1]) > -1: + print(f'loading {s_file}') + a_seg = skimage.io.imread(s_file) + d_label.update({s_scene:a_seg}) + os.chdir(cwd) + return(d_label) + +def crop_labels(d_crop,d_label,tu_dim,cropdir,s_name='Nuclei Segmentation Basins'): + """ + crop the segmentation basins (cell of nuceli) to same coord as images for veiwing in Napari + s_name = + """ + for s_scene, xy_cropcoor in d_crop.items(): + print(s_scene) + a_seg = d_label[s_scene] + a_crop = a_seg[(xy_cropcoor[1]):(xy_cropcoor[1]+tu_dim[1]),(xy_cropcoor[0]):(xy_cropcoor[0]+tu_dim[0])] + s_coor = f'x{xy_cropcoor[0]}y{xy_cropcoor[1]}.tif' + #crop file + s_file_new = f'{cropdir}/{s_scene}_{s_name.replace(" ","")}{s_coor}' + print(s_file_new) + skimage.io.imsave(s_file_new,a_crop) + + +def fmt(x, pos): + a, b = '{:.0e}'.format(x).split('e') + b = int(b) + return r'${} \times 10^{{{}}}$'.format(a, b) + +def array_roi_if(df_img,df_dapi,s_label='rounds',s_title='Title',tu_crop=(0,0,100,100),tu_array=(2,4),tu_fig=(10,20),tu_rescale=(0,0),i_expnorm=0,i_micron_per_pixel=.325): + """ + create a grid of images + df_img = dataframe of images with columns having image attributes + and index with image names + df_dapi = like df_img, but with the matching dapi images + s_label= attribute to label axes + s_title = x axis title + tu_crop = (upper left corner x, y , xlength, yheight) + tu_array = subplot array dimensions + tu_fig = size of figue + tu_rescale= range of rescaling + i_expnorm = normalize to an exposure time (requires 'exposure' column in dataframe + """ + cmap = mpl.colors.LinearSegmentedColormap.from_list('cmap', [(0,0,0),(0,1,0)], N=256, gamma=1.0) + fig, ax = plt.subplots(tu_array[0],tu_array[1],figsize=tu_fig,sharey=True, squeeze=False) # + ax = ax.ravel() + for ax_num, s_index in enumerate(df_img.index): + s_col_label = df_img.loc[s_index,s_label] + #load image, copr, rescale + a_image=skimage.io.imread(s_index) + a_dapi = skimage.io.imread((df_dapi).index[0])# & (df_dapi.rounds=='R1') + a_crop = a_image[(tu_crop[1]):(tu_crop[1]+tu_crop[3]),(tu_crop[0]):(tu_crop[0]+tu_crop[2])] + a_crop_dapi = a_dapi[(tu_crop[1]):(tu_crop[1]+tu_crop[3]),(tu_crop[0]):(tu_crop[0]+tu_crop[2])] + #a_crop_dapi = (a_crop_dapi/255).astype('int') + if i_expnorm > 0: + a_crop = a_crop/df_img.loc[s_index,'exposure']*i_expnorm + if tu_rescale==(0,0): + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range=(np.quantile(a_crop,0.03),1.5*np.quantile(a_crop,0.998)),out_range=(0, 255)) + tu_max = (np.quantile(a_crop,0.03),1.5*np.quantile(a_crop,0.998)) + else: + #print(f'original {a_crop.min()},{a_crop.max()}') + #print(f'rescale to {tu_rescale}') + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range = tu_rescale,out_range=(0,255)) + tu_max=tu_rescale + a_rescale_dapi = skimage.exposure.rescale_intensity(a_crop_dapi,in_range = (np.quantile(a_crop_dapi,0.03),2*np.quantile(a_crop_dapi,0.99)),out_range=(0,255)) + a_rescale_dapi = a_rescale_dapi.astype(np.uint8) + a_rescale = a_rescale.astype(np.uint8) + #2 color png + zdh = np.dstack((np.zeros_like(a_rescale), a_rescale, a_rescale_dapi)) + ax[ax_num].imshow(zdh) + ax[ax_num].set_title('') + ax[ax_num].set_ylabel('') + ax[ax_num].set_xlabel(s_col_label,fontsize = 'x-large') + if tu_rescale == (0,0): + if len(ax)>1: + ax[ax_num].set_xlabel(f'{s_col_label} ({int(np.quantile(a_crop,0.03))} - {int(1.5*np.quantile(a_crop,0.998))})') + ax[ax_num].set_xticklabels('') + #pixel to micron (apply after ax is returned) + #ax[0].set_yticklabels([str(int(re.sub(u"\u2212", "-", item.get_text()))*i_micron_per_pixel) for item in ax[0].get_yticklabels(minor=False)]) + plt.suptitle(s_title,y=0.93,size = 'xx-large',weight='bold') + plt.subplots_adjust(wspace=.05, hspace=.05) + # Now adding the colorbar + norm = mpl.colors.Normalize(vmin=tu_max[0],vmax=tu_max[1]) + sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm) + sm.set_array([]) + if len(ax) == 1: + cbaxes = fig.add_axes([.88, 0.125, 0.02, 0.75]) #[left, bottom, width, height] + plt.colorbar(sm, cax=cbaxes)#,format=ticker.FuncFormatter(fmt)) + plt.figtext(0.47,0.03,s_label.replace('_',' '),fontsize = 'x-large', weight='bold') + elif tu_rescale != (0,0): + cbaxes = fig.add_axes([.91, 0.15, 0.015, 0.7]) #[left, bottom, width, height] + plt.colorbar(sm, cax=cbaxes)#,format=ticker.FuncFormatter(fmt)) + plt.figtext(0.42,0.03,s_label.replace('_',' '),fontsize = 'x-large', weight='bold') + else: + print("Different ranges - can't use colorbar") + plt.figtext(0.43,0.03,s_label.replace('_',' '),fontsize = 'x-large', weight='bold') + + return(fig,ax) + +def multicolor_png(df_img,df_dapi,s_scene,d_overlay,d_crop,es_dim={'CD8','FoxP3','ER','AR'},es_bright={'Ki67','pHH3'},low_thresh=4000,high_thresh=0.999): + ''' + create RGB image with Dapi plus four - 6 channels + ''' + + d_result = {} + #print(s_scene) + tu_crop = d_crop[s_scene] + df_slide = df_img[df_img.scene == s_scene] + x=tu_crop[1] + y=tu_crop[0] + img_dapi = skimage.io.imread(df_dapi[df_dapi.scene==s_scene].path[0]) + a_crop = img_dapi[x:x+800,y:y+800] + a_rescale_dapi = skimage.exposure.rescale_intensity(a_crop,in_range=(np.quantile(img_dapi,0.2),1.5*np.quantile(img_dapi,high_thresh)),out_range=(0, 255)) + if 1.5*np.quantile(img_dapi,high_thresh) < low_thresh: + a_rescale_dapi = skimage.exposure.rescale_intensity(a_crop,in_range=(low_thresh/2,low_thresh),out_range=(0, 255)) + elif len(es_dim.intersection(set(['DAPI'])))==1: + new_thresh = float(str(high_thresh)[:-2]) + a_rescale_dapi = skimage.exposure.rescale_intensity(a_crop,in_range=(np.quantile(img_dapi,0.2),1.5*np.quantile(img_dapi,new_thresh)),out_range=(0, 255)) + elif len(es_bright.intersection(set(['DAPI'])))==1: + a_rescale_dapi = skimage.exposure.rescale_intensity(a_crop,in_range=(np.quantile(img_dapi,0.2),1.5*np.quantile(img_dapi,float(str(high_thresh) + '99'))),out_range=(0, 255)) + + #RGB + for s_type, ls_marker in d_overlay.items(): + #print(s_type) + zdh = np.dstack((np.zeros_like(a_rescale_dapi), np.zeros_like(a_rescale_dapi),a_rescale_dapi)) + for idx, s_marker in enumerate(ls_marker): + #print(s_marker) + s_index = df_slide[df_slide.marker == s_marker].index[0] + img = skimage.io.imread(df_slide.loc[s_index,'path']) + a_crop = img[x:x+800,y:y+800] + in_range = (np.quantile(a_crop,0.2),1.5*np.quantile(a_crop,high_thresh)) + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range=in_range,out_range=(0, 255)) + if 1.5*np.quantile(a_crop,high_thresh) < low_thresh: + #print('low thresh') + in_range=(low_thresh/2,low_thresh) + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range=in_range,out_range=(0, 255)) + elif len(es_dim.intersection(set([s_marker])))==1: + #print('dim') + new_thresh = float(str(high_thresh)[:-2]) + in_range=(np.quantile(a_crop,0.2),1.5*np.quantile(a_crop,new_thresh)) + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range=in_range,out_range=(0, 255)) + elif len(es_bright.intersection(set([s_marker])))==1: + #print('bright') + in_range=(np.quantile(a_crop,0.2),1.5*np.quantile(a_crop,float(str(high_thresh) + '99'))) + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range=in_range,out_range=(0, 255)) + + #print(f'low {int(in_range[0])} high {int(in_range[1])}') + if idx == 0: + zdh = zdh + np.dstack((np.zeros_like(a_rescale), a_rescale,np.zeros_like(a_rescale))) + + elif idx == 1: + zdh = zdh + np.dstack((a_rescale, a_rescale,np.zeros_like(a_rescale))) + + elif idx == 2: + zdh = zdh + np.dstack((a_rescale, np.zeros_like(a_rescale),np.zeros_like(a_rescale) )) + + elif idx == 3: + zdh = zdh + np.dstack((np.zeros_like(a_rescale), a_rescale, a_rescale)) + #print(zdh.min()) + zdh = zdh.clip(0,255) + zdh = zdh.astype('uint8') + #print(zdh.max()) + d_result.update({s_type:(ls_marker,zdh)}) + return(d_result) + +def roi_if_border(df_img,df_dapi,df_border,s_label='rounds',s_title='Title',tu_crop=(0,0,100,100),tu_array=(2,4),tu_fig=(10,20),tu_rescale=(0,0),i_expnorm=0,i_micron_per_pixel=.325): + """ + create a grid of images + df_img = dataframe of images with columns having image attributes + and index with image names + df_dapi = like df_img, but with the matching dapi images + df_border: index is border image file name + s_label= attribute to label axes + s_title = x axis title + tu_crop = (upper left corner x, y , xlength, yheight) + tu_array = subplot array dimensions + tu_fig = size of figue + tu_rescale= + i_expnorm = + """ + cmap = mpl.colors.LinearSegmentedColormap.from_list('cmap', [(0,0,0),(0,1,0)], N=256, gamma=1.0) + fig, ax = plt.subplots(tu_array[0],tu_array[1],figsize=tu_fig,sharey=True, squeeze=False) # + ax = ax.ravel() + for ax_num, s_index in enumerate(df_img.index): + s_col_label = df_img.loc[s_index,s_label] + #load image, copr, rescale + a_image=skimage.io.imread(s_index) + a_dapi = skimage.io.imread((df_dapi).index[0])# & (df_dapi.rounds=='R1') + a_crop = a_image[(tu_crop[1]):(tu_crop[1]+tu_crop[3]),(tu_crop[0]):(tu_crop[0]+tu_crop[2])] + a_crop_dapi = a_dapi[(tu_crop[1]):(tu_crop[1]+tu_crop[3]),(tu_crop[0]):(tu_crop[0]+tu_crop[2])] + #a_crop_dapi = (a_crop_dapi/255).astype('int') + if i_expnorm > 0: + a_crop = a_crop/df_img.loc[s_index,'exposure']*i_expnorm + if tu_rescale==(0,0): + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range=(np.quantile(a_crop,0.03),1.5*np.quantile(a_crop,0.998)),out_range=(0, 255)) + tu_max = (np.quantile(a_crop,0.03),1.5*np.quantile(a_crop,0.998)) + else: + print(f'original {a_crop.min()},{a_crop.max()}') + print(f'rescale to {tu_rescale}') + a_rescale = skimage.exposure.rescale_intensity(a_crop,in_range = tu_rescale,out_range=(0,255)) + tu_max=tu_rescale + a_rescale_dapi = skimage.exposure.rescale_intensity(a_crop_dapi,in_range = (np.quantile(a_crop_dapi,0.03),2*np.quantile(a_crop_dapi,0.99)),out_range=(0,255)) + a_rescale_dapi = a_rescale_dapi.astype(np.uint8) + a_rescale = a_rescale.astype(np.uint8) + #white border + s_border_index = df_border[df_border.marker==(df_img.loc[s_index,'marker'])].index[0] + a_border = skimage.io.imread(s_border_index) + a_crop_border = a_border[(tu_crop[1]):(tu_crop[1]+tu_crop[3]),(tu_crop[0]):(tu_crop[0]+tu_crop[2])] + mask = a_crop_border > 250 + #2 color png + zdh = np.dstack((np.zeros_like(a_rescale), a_rescale, a_rescale_dapi)) + zdh[mask] = 255 + #zdh = zdh.clip(0,255) + #zdh = zdh.astype('uint8') + ax[ax_num].imshow(zdh) + ax[ax_num].set_title('') + ax[ax_num].set_ylabel('') + ax[ax_num].set_xlabel(s_col_label,fontsize = 'x-large') + if tu_rescale == (0,0): + if len(ax)>1: + ax[ax_num].set_xlabel(f'{s_col_label} ({int(np.quantile(a_crop,0.03))} - {int(1.5*np.quantile(a_crop,0.998))})') + ax[ax_num].set_xticklabels('') + #pixel to micron (apply after ax is returned) + #ax[0].set_yticklabels([str(int(re.sub(u"\u2212", "-", item.get_text()))*i_micron_per_pixel) for item in ax[0].get_yticklabels(minor=False)]) + plt.suptitle(s_title,y=0.93,size = 'xx-large',weight='bold') + plt.subplots_adjust(wspace=.05, hspace=.05) + # Now adding the colorbar + norm = mpl.colors.Normalize(vmin=tu_max[0],vmax=tu_max[1]) + sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm) + sm.set_array([]) + if len(ax) == 1: + cbaxes = fig.add_axes([.88, 0.125, 0.02, 0.75]) #[left, bottom, width, height] + plt.colorbar(sm, cax = cbaxes) + plt.figtext(0.47,0.03,s_label.replace('_',' '),fontsize = 'x-large', weight='bold') + elif tu_rescale != (0,0): + cbaxes = fig.add_axes([.92, 0.175, 0.02, 0.64]) #[left, bottom, width, height] + plt.colorbar(sm, cax = cbaxes) + plt.figtext(0.42,0.03,s_label.replace('_',' '),fontsize = 'x-large', weight='bold') + else: + print("Different ranges - can't use colorbar") + plt.figtext(0.43,0.03,s_label.replace('_',' '),fontsize = 'x-large', weight='bold') + + return(fig,ax,a_crop_border) + diff --git a/mplex_image/normalize.py b/mplex_image/normalize.py new file mode 100755 index 0000000..2c03147 --- /dev/null +++ b/mplex_image/normalize.py @@ -0,0 +1,536 @@ +#from https://github.com/brentp/combat.py/blob/master/combat.py +import patsy +import sys +import numpy.linalg as la +import numpy as np +import pandas as pd +import sys +import matplotlib.pyplot as plt + +def aprior(gamma_hat): + m = gamma_hat.mean() + s2 = gamma_hat.var() + return (2 * s2 +m**2) / s2 + +def bprior(gamma_hat): + m = gamma_hat.mean() + s2 = gamma_hat.var() + return (m*s2+m**3)/s2 + +def it_sol(sdat, g_hat, d_hat, g_bar, t2, a, b, conv=0.0001): + n = (1 - np.isnan(sdat)).sum(axis=1) + g_old = g_hat.copy() + d_old = d_hat.copy() + + change = 1 + count = 0 + while change > conv: + #print g_hat.shape, g_bar.shape, t2.shape + g_new = postmean(g_hat, g_bar, n, d_old, t2) + sum2 = ((sdat - np.dot(g_new.values.reshape((g_new.shape[0], 1)), np.ones((1, sdat.shape[1])))) ** 2).sum(axis=1) + d_new = postvar(sum2, n, a, b) + + change = max((abs(g_new - g_old) / g_old).max(), (abs(d_new - d_old) / d_old).max()) + g_old = g_new #.copy() + d_old = d_new #.copy() + count = count + 1 + adjust = (g_new, d_new) + return adjust + +def postmean(g_hat, g_bar, n, d_star, t2): + return (t2*n*g_hat+d_star * g_bar) / (t2*n+d_star) + +def postvar(sum2, n, a, b): + return (0.5 * sum2 + b) / (n / 2.0 + a - 1.0) + +def design_mat(mod, numerical_covariates, batch_levels): + # require levels to make sure they are in the same order as we use in the + # rest of the script. + design = patsy.dmatrix("~ 0 + C(batch, levels=%s)" % str(batch_levels), + mod, return_type="dataframe") + + mod = mod.drop(["batch"], axis=1) + numerical_covariates = list(numerical_covariates) + sys.stderr.write("found %i batches\n" % design.shape[1]) + other_cols = [c for i, c in enumerate(mod.columns) + if not i in numerical_covariates] + factor_matrix = mod[other_cols] + design = pd.concat((design, factor_matrix), axis=1) + if numerical_covariates is not None: + sys.stderr.write("found %i numerical covariates...\n" + % len(numerical_covariates)) + for i, nC in enumerate(numerical_covariates): + cname = mod.columns[nC] + sys.stderr.write("\t{0}\n".format(cname)) + design[cname] = mod[mod.columns[nC]] + sys.stderr.write("found %i categorical variables:" % len(other_cols)) + sys.stderr.write("\t" + ", ".join(other_cols) + '\n') + return design + +def combat(data, batch, model=None, numerical_covariates=None): + """Correct for batch effects in a dataset + Parameters + ---------- + data : pandas.DataFrame + A (n_features, n_samples) dataframe of the expression or methylation + data to batch correct + batch : pandas.Series + A column corresponding to the batches in the data, with index same as + the columns that appear in ``data`` + model : patsy.design_info.DesignMatrix, optional + A model matrix describing metadata on the samples which could be + causing batch effects. If not provided, then will attempt to coarsely + correct just from the information provided in ``batch`` + numerical_covariates : list-like + List of covariates in the model which are numerical, rather than + categorical + Returns + ------- + corrected : pandas.DataFrame + A (n_features, n_samples) dataframe of the batch-corrected data + """ + if isinstance(numerical_covariates, str): + numerical_covariates = [numerical_covariates] + if numerical_covariates is None: + numerical_covariates = [] + + if model is not None and isinstance(model, pd.DataFrame): + model["batch"] = list(batch) + else: + model = pd.DataFrame({'batch': batch}) + + batch_items = model.groupby("batch").groups.items() + batch_levels = [k for k, v in batch_items] + batch_info = [v for k, v in batch_items] + n_batch = len(batch_info) + n_batches = np.array([len(v) for v in batch_info]) + n_array = float(sum(n_batches)) + + # drop intercept + drop_cols = [cname for cname, inter in ((model == 1).all()).iteritems() if inter == True] + drop_idxs = [list(model.columns).index(cdrop) for cdrop in drop_cols] + model = model[[c for c in model.columns if not c in drop_cols]] + numerical_covariates = [list(model.columns).index(c) if isinstance(c, str) else c + for c in numerical_covariates if not c in drop_cols] + + design = design_mat(model, numerical_covariates, batch_levels) + + sys.stderr.write("Standardizing Data across genes.\n") + #error shapes (3,7200) and (26,7200) not aligned: 7200 (dim 1) != 26 (dim 0) + B_hat = np.dot(np.dot(la.inv(np.dot(design.T, design)), design.T), data.T) #data.T + grand_mean = np.dot((n_batches / n_array).T, B_hat[:n_batch,:]) + var_pooled = np.dot(((data - np.dot(design, B_hat).T)**2), np.ones((int(n_array), 1)) / int(n_array)) + + stand_mean = np.dot(grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array)))) + tmp = np.array(design.copy()) + tmp[:,:n_batch] = 0 + stand_mean += np.dot(tmp, B_hat).T + + s_data = ((data - stand_mean) / np.dot(np.sqrt(var_pooled), np.ones((1, int(n_array))))) + + sys.stderr.write("Fitting L/S model and finding priors\n") + batch_design = design[design.columns[:n_batch]] + gamma_hat = np.dot(np.dot(la.inv(np.dot(batch_design.T, batch_design)), batch_design.T), s_data.T) + + delta_hat = [] + + for i, batch_idxs in enumerate(batch_info): + #batches = [list(model.columns).index(b) for b in batches] + delta_hat.append(s_data[batch_idxs].var(axis=1)) + + gamma_bar = gamma_hat.mean(axis=1) + t2 = gamma_hat.var(axis=1) + + + a_prior = list(map(aprior, delta_hat)) + b_prior = list(map(bprior, delta_hat)) + + sys.stderr.write("Finding parametric adjustments\n") + gamma_star, delta_star = [], [] + for i, batch_idxs in enumerate(batch_info): + #print '18 20 22 28 29 31 32 33 35 40 46' + #print batch_info[batch_id] + + temp = it_sol(s_data[batch_idxs], gamma_hat[i], + delta_hat[i], gamma_bar[i], t2[i], a_prior[i], b_prior[i]) + + gamma_star.append(temp[0]) + delta_star.append(temp[1]) + + sys.stdout.write("Adjusting data\n") + bayesdata = s_data + gamma_star = np.array(gamma_star) + delta_star = np.array(delta_star) + + + for j, batch_idxs in enumerate(batch_info): + + dsq = np.sqrt(delta_star[j,:]) + dsq = dsq.reshape((len(dsq), 1)) + denom = np.dot(dsq, np.ones((1, n_batches[j]))) + numer = np.array(bayesdata[batch_idxs] - np.dot(batch_design.loc[batch_idxs], gamma_star).T) + + bayesdata[batch_idxs] = numer / denom + + vpsq = np.sqrt(var_pooled).reshape((len(var_pooled), 1)) + bayesdata = bayesdata * np.dot(vpsq, np.ones((1, int(n_array)))) + stand_mean + + return bayesdata + +#adapted from https://github.com/brentp/combat.py/blob/master/combat.py + + +def combat_fit(data, batch, model=None, numerical_covariates=None): + """Correct for batch effects in a dataset + Parameters + ---------- + data : pandas.DataFrame + A (n_features, n_samples) dataframe of the expression or methylation + data to batch correct + batch : pandas.Series + A column corresponding to the batches in the data, with index same as + the columns that appear in ``data`` + model : patsy.design_info.DesignMatrix, optional + A model matrix describing metadata on the samples which could be + causing batch effects. If not provided, then will attempt to coarsely + correct just from the information provided in ``batch`` + numerical_covariates : list-like + List of covariates in the model which are numerical, rather than + categorical + Returns + ------- + gamma_star : centering parameters from combat fitting + delta_star : scaling parameters from combat fitting + stand_mean: pooled mean of batches + var_pooled: pooled variance of batches + """ + if isinstance(numerical_covariates, str): + numerical_covariates = [numerical_covariates] + if numerical_covariates is None: + numerical_covariates = [] + + if model is not None and isinstance(model, pd.DataFrame): + model["batch"] = list(batch) + else: + model = pd.DataFrame({'batch': batch}) + + batch_items = model.groupby("batch").groups.items() + batch_levels = [k for k, v in batch_items] + batch_info = [v for k, v in batch_items] + n_batch = len(batch_info) + n_batches = np.array([len(v) for v in batch_info]) + n_array = float(sum(n_batches)) + + # drop intercept + drop_cols = [cname for cname, inter in ((model == 1).all()).iteritems() if inter == True] + drop_idxs = [list(model.columns).index(cdrop) for cdrop in drop_cols] + model = model[[c for c in model.columns if not c in drop_cols]] + numerical_covariates = [list(model.columns).index(c) if isinstance(c, str) else c + for c in numerical_covariates if not c in drop_cols] + + design = design_mat(model, numerical_covariates, batch_levels) + + sys.stderr.write("Standardizing Data across genes.\n") + B_hat = np.dot(np.dot(la.inv(np.dot(design.T, design)), design.T), data.T) + grand_mean = np.dot((n_batches / n_array).T, B_hat[:n_batch,:]) + var_pooled = np.dot(((data - np.dot(design, B_hat).T)**2), np.ones((int(n_array), 1)) / int(n_array)) + + stand_mean = np.dot(grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array)))) + tmp = np.array(design.copy()) + tmp[:,:n_batch] = 0 + stand_mean += np.dot(tmp, B_hat).T + + s_data = ((data - stand_mean) / np.dot(np.sqrt(var_pooled), np.ones((1, int(n_array))))) + + sys.stderr.write("Fitting L/S model and finding priors\n") + batch_design = design[design.columns[:n_batch]] + gamma_hat = np.dot(np.dot(la.inv(np.dot(batch_design.T, batch_design)), batch_design.T), s_data.T) + + delta_hat = [] + + for i, batch_idxs in enumerate(batch_info): + delta_hat.append(s_data[batch_idxs].var(axis=1)) + + gamma_bar = gamma_hat.mean(axis=1) + t2 = gamma_hat.var(axis=1) + + + a_prior = list(map(aprior, delta_hat)) + b_prior = list(map(bprior, delta_hat)) + + sys.stderr.write("Finding parametric adjustments\n") + gamma_star, delta_star = [], [] + for i, batch_idxs in enumerate(batch_info): + temp = it_sol(s_data[batch_idxs], gamma_hat[i], + delta_hat[i], gamma_bar[i], t2[i], a_prior[i], b_prior[i]) + + gamma_star.append(temp[0]) + delta_star.append(temp[1]) + #just retrun one stand_mean array + stand_mean = stand_mean[:,0] + return(gamma_star, delta_star, stand_mean, var_pooled) + +def combat_transform(data, batch, gamma_star, delta_star, stand_mean, var_pooled,model=None, numerical_covariates=None): + """Correct for batch effects in a dataset + Parameters + ---------- + data : pandas.DataFrame + A (n_features, n_samples) dataframe of the expression or methylation + data to batch correct + batch : pandas.Series + A column corresponding to the batches in the data, with index same as + the columns that appear in ``data`` + gamma_star : centering parameters from combat fitting + delta_star : scaling parameters from combat fitting + stand_mean: pooled mean of batches + var_pooled: pooled variance of batches + model : patsy.design_info.DesignMatrix, optional + A model matrix describing metadata on the samples which could be + causing batch effects. If not provided, then will attempt to coarsely + correct just from the information provided in ``batch`` + numerical_covariates : list-like + List of covariates in the model which are numerical, rather than + categorical + Returns + ------- + corrected : pandas.DataFrame + A (n_features, n_samples) dataframe of the batch-corrected data + """ + #get design + if isinstance(numerical_covariates, str): + numerical_covariates = [numerical_covariates] + if numerical_covariates is None: + numerical_covariates = [] + + if model is not None and isinstance(model, pd.DataFrame): + model["batch"] = list(batch) + else: + model = pd.DataFrame({'batch': batch}) + batch_items = model.groupby("batch").groups.items() + batch_levels = [k for k, v in batch_items] + batch_info = [v for k, v in batch_items] + n_batch = len(batch_info) + n_batches = np.array([len(v) for v in batch_info]) + n_array = float(sum(n_batches)) + # drop intercept + drop_cols = [cname for cname, inter in ((model == 1).all()).iteritems() if inter == True] + drop_idxs = [list(model.columns).index(cdrop) for cdrop in drop_cols] + model = model[[c for c in model.columns if not c in drop_cols]] + numerical_covariates = [list(model.columns).index(c) if isinstance(c, str) else c + for c in numerical_covariates if not c in drop_cols] + design = design_mat(model, numerical_covariates, batch_levels) + #standardize + sys.stderr.write("Standardizing Data across genes.\n") + + #reshape stand mean + stand_mean = np.dot(stand_mean.T.reshape((len(stand_mean), 1)), np.ones((1, int(data.shape[1])))) + s_data = ((data - stand_mean) / np.dot(np.sqrt(var_pooled), np.ones((1, int(n_array))))) + batch_design = design[design.columns[:n_batch]] + # adjust data + sys.stdout.write("Adjusting data\n") + bayesdata = s_data + gamma_star = np.array(gamma_star) + delta_star = np.array(delta_star) + #for each batch + for j, batch_idxs in enumerate(batch_info): + + dsq = np.sqrt(delta_star[j,:]) + dsq = dsq.reshape((len(dsq), 1)) + denom = np.dot(dsq, np.ones((1, n_batches[j]))) #divide by sqrt delta_star + numer = np.array(bayesdata[batch_idxs] - np.dot(batch_design.loc[batch_idxs], gamma_star).T) #subtract gamma_star + + bayesdata[batch_idxs] = numer / denom + #multiply by square root of variance and add mean + vpsq = np.sqrt(var_pooled).reshape((len(var_pooled), 1)) + bayesdata = bayesdata * np.dot(vpsq, np.ones((1, int(n_array)))) + stand_mean + return bayesdata + + +def combat_fit_old(data, batch, model=None, numerical_covariates=None): + """Correct for batch effects in a dataset + Parameters + ---------- + data : pandas.DataFrame + A (n_features, n_samples) dataframe of the expression or methylation + data to batch correct + batch : pandas.Series + A column corresponding to the batches in the data, with index same as + the columns that appear in ``data`` + model : patsy.design_info.DesignMatrix, optional + A model matrix describing metadata on the samples which could be + causing batch effects. If not provided, then will attempt to coarsely + correct just from the information provided in ``batch`` + numerical_covariates : list-like + List of covariates in the model which are numerical, rather than + categorical + Returns + ------- + gamma_star : centering parameters from combat fitting + delta_star : scaling parameters from combat fitting + """ + if isinstance(numerical_covariates, str): + numerical_covariates = [numerical_covariates] + if numerical_covariates is None: + numerical_covariates = [] + + if model is not None and isinstance(model, pd.DataFrame): + model["batch"] = list(batch) + else: + model = pd.DataFrame({'batch': batch}) + + batch_items = model.groupby("batch").groups.items() + batch_levels = [k for k, v in batch_items] + batch_info = [v for k, v in batch_items] + n_batch = len(batch_info) + n_batches = np.array([len(v) for v in batch_info]) + n_array = float(sum(n_batches)) + + # drop intercept + drop_cols = [cname for cname, inter in ((model == 1).all()).iteritems() if inter == True] + drop_idxs = [list(model.columns).index(cdrop) for cdrop in drop_cols] + model = model[[c for c in model.columns if not c in drop_cols]] + numerical_covariates = [list(model.columns).index(c) if isinstance(c, str) else c + for c in numerical_covariates if not c in drop_cols] + + design = design_mat(model, numerical_covariates, batch_levels) + + sys.stderr.write("Standardizing Data across genes.\n") + B_hat = np.dot(np.dot(la.inv(np.dot(design.T, design)), design.T), data.T) + grand_mean = np.dot((n_batches / n_array).T, B_hat[:n_batch,:]) + var_pooled = np.dot(((data - np.dot(design, B_hat).T)**2), np.ones((int(n_array), 1)) / int(n_array)) + + stand_mean = np.dot(grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array)))) + tmp = np.array(design.copy()) + tmp[:,:n_batch] = 0 + stand_mean += np.dot(tmp, B_hat).T + + s_data = ((data - stand_mean) / np.dot(np.sqrt(var_pooled), np.ones((1, int(n_array))))) + + sys.stderr.write("Fitting L/S model and finding priors\n") + batch_design = design[design.columns[:n_batch]] + gamma_hat = np.dot(np.dot(la.inv(np.dot(batch_design.T, batch_design)), batch_design.T), s_data.T) + + delta_hat = [] + + for i, batch_idxs in enumerate(batch_info): + delta_hat.append(s_data[batch_idxs].var(axis=1)) + + gamma_bar = gamma_hat.mean(axis=1) + t2 = gamma_hat.var(axis=1) + + + a_prior = list(map(aprior, delta_hat)) + b_prior = list(map(bprior, delta_hat)) + + sys.stderr.write("Finding parametric adjustments\n") + gamma_star, delta_star = [], [] + for i, batch_idxs in enumerate(batch_info): + temp = it_sol(s_data[batch_idxs], gamma_hat[i], + delta_hat[i], gamma_bar[i], t2[i], a_prior[i], b_prior[i]) + + gamma_star.append(temp[0]) + delta_star.append(temp[1]) + return(gamma_star, delta_star) + +def combat_transform_old(data, batch, gamma_star, delta_star,model=None, numerical_covariates=None): + """Correct for batch effects in a dataset + Parameters + ---------- + data : pandas.DataFrame + A (n_features, n_samples) dataframe of the expression or methylation + data to batch correct + batch : pandas.Series + A column corresponding to the batches in the data, with index same as + the columns that appear in ``data`` + gamma_star : centering parameters from combat fitting + delta_star : scaling parameters from combat fitting + model : patsy.design_info.DesignMatrix, optional + A model matrix describing metadata on the samples which could be + causing batch effects. If not provided, then will attempt to coarsely + correct just from the information provided in ``batch`` + numerical_covariates : list-like + List of covariates in the model which are numerical, rather than + categorical + Returns + ------- + corrected : pandas.DataFrame + A (n_features, n_samples) dataframe of the batch-corrected data + """ + #get design + if isinstance(numerical_covariates, str): + numerical_covariates = [numerical_covariates] + if numerical_covariates is None: + numerical_covariates = [] + + if model is not None and isinstance(model, pd.DataFrame): + model["batch"] = list(batch) + else: + model = pd.DataFrame({'batch': batch}) + batch_items = model.groupby("batch").groups.items() + batch_levels = [k for k, v in batch_items] + batch_info = [v for k, v in batch_items] + n_batch = len(batch_info) + n_batches = np.array([len(v) for v in batch_info]) + n_array = float(sum(n_batches)) + # drop intercept + drop_cols = [cname for cname, inter in ((model == 1).all()).iteritems() if inter == True] + drop_idxs = [list(model.columns).index(cdrop) for cdrop in drop_cols] + model = model[[c for c in model.columns if not c in drop_cols]] + numerical_covariates = [list(model.columns).index(c) if isinstance(c, str) else c + for c in numerical_covariates if not c in drop_cols] + design = design_mat(model, numerical_covariates, batch_levels) + #standardize + sys.stderr.write("Standardizing Data across genes.\n") + B_hat = np.dot(np.dot(la.inv(np.dot(design.T, design)), design.T), data.T) + grand_mean = np.dot((n_batches / n_array).T, B_hat[:n_batch,:]) + var_pooled = np.dot(((data - np.dot(design, B_hat).T)**2), np.ones((int(n_array), 1)) / int(n_array)) + + stand_mean = np.dot(grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array)))) + tmp = np.array(design.copy()) + tmp[:,:n_batch] = 0 + stand_mean += np.dot(tmp, B_hat).T + s_data = ((data - stand_mean) / np.dot(np.sqrt(var_pooled), np.ones((1, int(n_array))))) + batch_design = design[design.columns[:n_batch]] + # adjust data + sys.stdout.write("Adjusting data\n") + bayesdata = s_data + gamma_star = np.array(gamma_star) + delta_star = np.array(delta_star) + #for each batch + for j, batch_idxs in enumerate(batch_info): + + dsq = np.sqrt(delta_star[j,:]) + dsq = dsq.reshape((len(dsq), 1)) + denom = np.dot(dsq, np.ones((1, n_batches[j]))) #divide by sqrt delta_star + numer = np.array(bayesdata[batch_idxs] - np.dot(batch_design.loc[batch_idxs], gamma_star).T) #subtract gamma_star + + bayesdata[batch_idxs] = numer / denom + #multiply by square root of variance and add mean + vpsq = np.sqrt(var_pooled).reshape((len(var_pooled), 1)) + bayesdata = bayesdata * np.dot(vpsq, np.ones((1, int(n_array)))) + stand_mean + return bayesdata + +def plot_histograms(df_norm,df,s_train,s_tissue): + ''' + for each marker, return a histogram of trianing data and transformed data (df_norm) + ''' + bins=50 + d_fig = {} + for s_marker in df_norm.columns[df_norm.dtypes=='float64']: + print(s_marker) + fig,ax=plt.subplots(2,1,figsize = (3,4)) + for idxs, s_batch in enumerate(sorted(set(df_norm.batch))): + df_batch = df_norm[(df_norm.batch==s_batch)].loc[:,s_marker] + if len(df_batch.dropna()) == 0: + continue + ax[0].hist(df.loc[df.index.str.contains(s_batch),s_marker],bins=bins,alpha=0.4, color=f'C{idxs}') + ax[1].hist(df_batch,bins=bins,alpha=0.4, color=f'C{idxs}',label=s_batch) + ax[0].set_yscale('log') + ax[1].set_yscale('log') + ax[0].set_title(f'{s_marker.split("_")[0]}: Raw Data') + ax[1].set_title(f'{s_marker.split("_")[0]}: Combat') + ax[1].legend() + plt.tight_layout() + plt.close() + d_fig.update({s_marker:fig}) + return(d_fig) \ No newline at end of file diff --git a/mplex_image/ometiff.py b/mplex_image/ometiff.py new file mode 100755 index 0000000..9986c6d --- /dev/null +++ b/mplex_image/ometiff.py @@ -0,0 +1,76 @@ +#### +# title: mpimage.py +# +# language: Python3.6 +# date: 2019-05-00 +# license: GPL>=v3 +# author: Jenny +# +# description: +# python3 library to display, normalize and crop multiplex images +#### + +#libraries +import matplotlib as mpl +mpl.use('agg') +import matplotlib.pyplot as plt +import numpy as np +import os +import skimage +import pandas as pd +#import bioformats +import re +import shutil +from itertools import chain +import matplotlib.ticker as ticker + +os.chdir('/home/groups/graylab_share/OMERO.rdsStore/engje/Data/cmIF/') +from apeer_ometiff_library import omexmlClass + +#functions + +def gen_xml(array, channel_names): + ''' + copy and modify from apeer ome tiff + ls_marker + ''' + #for idx, s_marker in enumerate(ls_marker): + # old = bytes(f'Name="C:{idx}"','utf-8') + # new = bytes(f'Name="{s_marker}"','utf-8') + # s_xml = s_xml.replace(old,new,-1) + #Dimension order is assumed to be TZCYX + dim_order = "TZCYX" + + metadata = omexmlClass.OMEXML() + shape = array.shape + assert ( len(shape) == 5), "Expected array of 5 dimensions" + + metadata.image().set_Name("IMAGE") + metadata.image().set_ID("0") + + pixels = metadata.image().Pixels + pixels.ome_uuid = metadata.uuidStr + pixels.set_ID("0") + + pixels.channel_count = shape[2] + + pixels.set_SizeT(shape[0]) + pixels.set_SizeZ(shape[1]) + pixels.set_SizeC(shape[2]) + pixels.set_SizeY(shape[3]) + pixels.set_SizeX(shape[4]) + + pixels.set_DimensionOrder(dim_order[::-1]) + + pixels.set_PixelType(omexmlClass.get_pixel_type(array.dtype)) + + for i in range(pixels.SizeC): + pixels.Channel(i).set_ID("Channel:0:" + str(i)) + pixels.Channel(i).set_Name(channel_names[i]) + + for i in range(pixels.SizeC): + pixels.Channel(i).set_SamplesPerPixel(1) + + pixels.populate_TiffData() + + return metadata.to_xml().encode() diff --git a/mplex_image/preprocess.py b/mplex_image/preprocess.py new file mode 100755 index 0000000..a54e54b --- /dev/null +++ b/mplex_image/preprocess.py @@ -0,0 +1,705 @@ +#### +# title: preprocess.py +# +# language: Python3.6 +# date: 2019-06-00 +# license: GPL>=v3 +# author: Jenny +# +# description: +# python3 library to prepare images and other inputs for guillaumes segmentation software +#### + +#libraries +import pandas as pd +import matplotlib as mpl +mpl.use('agg') +import matplotlib.pyplot as plt +import numpy as np +import os +import skimage +import shutil +import re + +#set src path (CHANGE ME) +s_src_path = '/home/groups/graylab_share/OMERO.rdsStore/engje/Data/cmIF' +s_work_path = '/home/groups/graylab_share/Chin_Lab/ChinData/Work/engje' + +# function +# import importlib +# importlib.reload(preprocess) + +def check_names(df_img,s_type='tiff'): + """ + (CHANGE ME) + Based on filenames in segment folder, + checks marker names against standard list of biomarkers + returns a dataframe with Rounds Cycles Info, and sets of wrong and correct names + Input: s_find = string that will be unique to one scene to check in the folder + """ + if s_type == 'tiff': + es_names = set(df_img.marker) + elif s_type == 'czi': + lls_marker = [item.split('.') for item in df_img.markers] + es_names = set([item for sublist in lls_marker for item in sublist]) + else : + print('Unknown type') + es_standard = {'DAPI','PDL1','pERK','CK19','pHH3','CK14','Ki67','Ecad','PCNA','HER2','ER','CD44', + 'aSMA','AR','pAKT','LamAC','CK5','EGFR','pRB','FoxP3','CK7','PDPN','CD4','PgR','Vim', + 'CD8','CD31','CD45','panCK','CD68','PD1','CD20','CK8','cPARP','ColIV','ColI','CK17', + 'H3K4','gH2AX','CD3','H3K27','53BP1','BCL2','GRNZB','LamB1','pS6RP','BAX','RAD51', + 'R0c2','R0c3','R0c4','R0c5','R5Qc2','R5Qc3','R5Qc4','R5Qc5','R11Qc2','R11Qc3','R11Qc4','R11Qc5', + 'R7Qc2','R7Qc3','R7Qc4','R7Qc5','PDL1ab','PDL1d','R14Qc2','R14Qc3','R14Qc4','R14Qc5', + 'R8Qc2','R8Qc3','R8Qc4','R8Qc5','R12Qc2','R12Qc3','R12Qc4','R12Qc5','PgRc4','R1c2','CCND1', + 'Glut1','CoxIV','LamB2','S100','BMP4','BMP2','BMP6','pS62MYC', 'CGA', 'p63', 'SYP','PDGFRa', 'HIF1a','CC3', + 'MUC1','CAV1','MSH2','CSF1R','R13Qc4', 'R13Qc5', 'R13Qc3', 'R13Qc2','R10Qc2','R10Qc3','R10Qc4','R10Qc5', + 'R6Qc2', 'R6Qc3','R6Qc4', 'R6Qc5', 'TUBB3', 'CD90', 'GATA3'}#,'PDGFRB'CD66b (Neutrophils) + #HLA class II or CD21(Dendritic cells) + #BMP4 Fibronectin, CD11b (dendritic, macrophage/monocyte/granulocyte) CD163 (macrophages) + #CD83 (dendritic cells) FAP + es_wrong = es_names - es_standard + es_right = es_standard.intersection(es_names) + print(f'Wrong names {es_wrong}') + print(f' Right names {es_right}') + return(es_wrong) + +def copy_dapis(s_r_old='-R11_',s_r_new='-R91_',s_c_old='_c1_',s_c_new='_c2_',s_find='_c1_ORG.tif',b_test=True,type='org'): + """ + copy specified round of dapi, rename with new round and color + Input: + s_r_old = old round + s_r_new = new round on copied DAPI + s_c_old = old color + s_c_new = new color on copied DAPI + s_find= how to identify dapis i.e. '_c1_ORG.tif' + b_test=True if testing only + """ + i_dapi = re.sub("[^0-9]", "", s_r_old) + ls_test = [] + for s_file in os.listdir(): + if s_file.find(s_find) > -1: + if s_file.find(s_r_old) > -1: + s_file_round = s_file.replace(s_r_old,s_r_new) + s_file_color = s_file_round.replace(s_c_old,s_c_new) + if type=='org': + s_file_dapi = s_file_color.replace(s_file_color.split("_")[1],f'DAPI{i_dapi}.DAPI{i_dapi}.DAPI{i_dapi}.DAPI{i_dapi}') + else: + s_file_dapi=s_file_color + ls_test = ls_test + [s_file] + if b_test: + print(f'copied file {s_file} \t and named {s_file_dapi}') + else: + print(f'copied file {s_file} \t and named {s_file_dapi}') + shutil.copyfile(s_file, s_file_dapi) + + print(f'total number of files changed is {len(ls_test)}') + +def copy_markers(df_img, s_original = 'panCK', ls_copy = ['CK19','CK5','CK7','CK14'],i_last_round = 97, b_test=True, type = 'org'): + """ + copy specified marker image, rename with new round and color (default c2) and marker name + Input: + s_original = marker to copy + df_img = dataframe with images + ls_copy = list of fake channels to make + + b_test=True if testing only + """ + df_copy = df_img[df_img.marker==s_original] + ls_test = [] + for s_index in df_copy.index: + s_round = df_img.loc[s_index,'rounds'] + for idx, s_copy in enumerate(ls_copy): + i_round = i_last_round + 1 + idx + s_round = df_img.loc[s_index,'rounds'] + s_roundnum = re.sub("[^0-9]", "", s_round) + s_round_pre = s_round.replace(s_roundnum,'') + s_file_round = s_index.replace(df_img.loc[s_index,'rounds'],f'{s_round_pre}{i_round}') + s_file_color = s_file_round.replace(f'_{s_round}_',f'_c{i_round}_') + if type == 'org': + s_file_dapi = s_file_color.replace(s_file_color.split("_")[1],f'{s_copy}.{s_copy}.{s_copy}.{s_copy}') + else: + s_file_dapi = s_file_color.replace(f'_{s_original}_',f'_{s_copy}_') + ls_test = ls_test + [s_index] + if b_test: + print(f'copied file {s_index} \t and named {s_file_dapi}') + else: + print(f'copied file {s_index} \t and named {s_file_dapi}') + shutil.copyfile(s_index, s_file_dapi) + print(f'total number of files changed is {len(ls_test)}') + +def dchange_fname(d_rename={'_oldstring_':'_newstring_'},b_test=True): + """ + replace anything in file name, based on dictionary of key = old + values = new + Input + """ + #d_rename = {'Registered-R11_CD34.AR.':'Registered-R11_CD34.ARcst.','FoxP3b':'FoxP3bio'} + for s_key,s_value in d_rename.items(): + s_old=s_key + s_new=s_value + #test + if b_test: + ls_test = [] + for s_file in os.listdir(): + if s_file.find(s_old) > -1: + s_file_print = s_file + ls_test = ls_test + [s_file] + len(ls_test) + s_file_new = s_file.replace(s_old,s_new) + #print(f'changed file {s_file}\tto {s_file_new}') + if len(ls_test)!=0: + print(f'changed file {s_file_print}\tto {s_file_new}') + print(f'total number of files changed is {len(ls_test)}') + #really rename + else: + ls_test = [] + for s_file in os.listdir(): + if s_file.find(s_old) > -1: + s_file_print = s_file + ls_test = ls_test + [s_file] + len(ls_test) + s_file_new = s_file.replace(s_old,s_new) + #print(f'changed file {s_file}\tto {s_file_new}') + os.rename(s_file, s_file_new) #comment out this line to test + if len(ls_test)!=0: + print(f'changed file {s_file_print}\tto {s_file_new}') + print(f'total number of files changed is {len(ls_test)}') + +def csv_change_fname(i_scene_len=2, b_test=True): + ''' + give a csv with wrong_round and correct scene names + make a Renamed folder + the correct scene is added after, as +correct + ''' + df_test = pd.read_csv(f'FinalSceneNumbers.csv',header=0) + df_test = df_test.astype(str)#(works!) + if i_scene_len == 2: + df_scene = df_test.applymap('{:0>2}'.format) + elif i_scene_len == 3: + df_test.replace('nan','',inplace=True) + df_test.replace(to_replace = "\.0+$",value = "", regex = True,inplace=True) + df_scene = df_test.applymap('{:0>3}'.format) + else: + df_scene = df_test #.applymap('{:0>3}'.format) + #for each round with wrong names + for s_wrong in df_scene.columns[df_scene.columns.str.contains('wrong')]: + for s_file in os.listdir(): + #find files in that round + if s_file.find(f'R{s_wrong.split("_")[1]}_') > -1: + #print(s_file) + #for each scene + for s_index in df_scene.index: + s_wrong_scene = df_scene.loc[s_index,s_wrong] + if s_file.find(f'-Scene-{s_wrong_scene}') > -1: + s_correct = df_scene.loc[s_index,'correct'] + print(s_correct) + s_replace = s_file.replace(f'-Scene-{s_wrong_scene}', f'-Scene-{s_wrong_scene}+{s_correct}') + s_file_new = f"./Renamed/{s_replace}" + + if b_test: + print(f'changed file {s_file} to {s_file_new}') + else: + os.rename(s_file, s_file_new) + print(f'changed file {s_file} to {s_file_new}') + return(df_test) + +def check_seg_markers(df_img,d_segment = {'CK19':1002,'CK5':5002,'CD45':2002,'Ecad':802,'CD44':1202,'CK7':2002,'CK14':502}, i_rows=1, t_figsize=(20,10)): + """ + This script makes binarizedoverviews of all the specified segmentation markers + with specified thresholds, and outputs a rounds cycles table + Input: df_dapi: output of mpimage.parse_org() + d_segment: segmentation marker names and thresholds + i_rows = number or rows in figure + t_figsize = (x, y) in inches size of figure + Output: dictionary + """ + d_result = {} + for s_key,i_item in d_segment.items(): + #find all segmentation marker slides + df_img_seg = df_img[df_img.marker==s_key] + fig,ax = plt.subplots(i_rows,(len(df_img_seg)+(i_rows-1))//i_rows, figsize = t_figsize, squeeze=False) + ax = ax.ravel() + for idx,s_scene in enumerate(sorted(df_img_seg.index.tolist())): + print(f'Processing {s_scene}') + im_low = skimage.io.imread(s_scene) + im = skimage.exposure.rescale_intensity(im_low,in_range=(i_item,i_item+1)) + ax[idx].imshow(im, cmap='gray') + s_round = s_scene.split('Scene')[1].split('_')[0] + ax[idx].set_title(f'{s_key} Scene{s_round} min={i_item}',{'fontsize':12}) + plt.tight_layout() + d_result.update({s_key:fig}) + return(d_result) + +def checkall_seg_markers(df_img,d_segment = {'CK19':1002,'CK5':5002,'CD45':2002,'Ecad':802,'CD44':1202,'CK7':2002,'CK14':502}, i_rows=2, t_figsize=(15,10)): + """ + This script makes binarizedoverviews of all the specified segmentation markers + with specified thresholds, and it puts all segmentation markers in one figure + Input: df_dapi: output of mpimage.parse_org() + d_segment: segmentation marker names and thresholds + i_rows = number or rows in figure + t_figsize = (x, y) in inches size of figure + Output: dictionary + """ + es_seg = set([s_key for s_key,i_item in d_segment.items()]) + df_img_seg = df_img[df_img.marker.isin(es_seg)] + fig,ax = plt.subplots(i_rows,(len(es_seg)+(i_rows-1))//i_rows, figsize = t_figsize, squeeze=False) + ax = ax.ravel() + for idx,s_scene in enumerate(sorted(df_img_seg.index.tolist())): + s_key = df_img.loc[s_scene].marker + i_item = d_segment[s_key] + print(f'Processing {s_scene}') + im_low = skimage.io.imread(s_scene) + im = skimage.exposure.rescale_intensity(im_low,in_range=(i_item,i_item+1)) + ax[idx].imshow(im, cmap='gray') + s_round = s_scene.split('Scene')[1].split('_')[0] + ax[idx].set_title(f'{s_key} Scene{s_round} min={i_item}',{'fontsize':12}) + plt.tight_layout() + #d_result.update({s_key:fig}) + return(fig) + +def rounds_cycles(s_find='-Scene-001_c', d_segment = {'CK19':1002,'CK5':5002,'CD45':4502,'Ecad':802,'CD44':1202,'CK7':2002,'CK14':502}): + """ + Based on filenames in segment folder, makes a dataframe with Rounds Cycles Info + """ + ls_marker = [] + df_dapi = pd.DataFrame() #(columns=['rounds','colors','minimum','maximum','exposure','refexp','location']) + for s_name in sorted(os.listdir()): + if s_name.find(s_find) > -1: + s_color = s_name.split('_')[3] + if s_color != 'c1': + #print(s_name) + if s_color == 'c2': + s_marker = s_name.split('_')[1].split('.')[0] + elif s_color == 'c3': + s_marker = s_name.split('_')[1].split('.')[1] + elif s_color == 'c4': + s_marker = s_name.split('_')[1].split('.')[2] + elif s_color == 'c5': + s_marker = s_name.split('_')[1].split('.')[3] + else: + print('Error: unrecognized channel name') + s_marker = 'error' + ls_marker.append(s_marker) + df_marker = pd.DataFrame(index = [s_marker],columns=['rounds','colors','minimum','maximum','exposure','refexp','location']) + df_marker.loc[s_marker,'rounds'] = s_name.split('_')[0].split('Registered-')[1] + df_marker.loc[s_marker,'colors'] = s_name.split('_')[3] + df_marker.loc[s_marker,'minimum'] = 1003 + df_marker.loc[s_marker,'maximum'] = 65535 + df_marker.loc[s_marker,'exposure'] = 100 + df_marker.loc[s_marker,'refexp'] = 100 + df_marker.loc[s_marker,'location'] = 'All' + df_dapi = df_dapi.append(df_marker) + for s_key,i_item in d_segment.items(): + df_dapi.loc[s_key,'minimum'] = i_item + #if len(ls_marker) != len(set(df_marker.index)): + # print('Check for repeated biomarkers!') + for s_marker in ls_marker: + if (np.array([s_marker == item for item in ls_marker]).sum()) != 1: + print('Repeated marker!/n') + print(s_marker) + + return(df_dapi, ls_marker) + +def cluster_java(s_dir='JE1',s_sample='SampleID',imagedir='PathtoImages',segmentdir='PathtoSegmentation',type='exacloud',b_segment=True,b_TMA=True): + """ + makes specific changes to files in Jenny's Work directories to result in Cluster.java file + s_dir = directory to make cluster.java file in + s_sample = unique sample ID + imagedir = full /path/to/images + type = 'exacloud' or 'eppec' (different make file settings) + b_TMA = True if tissue is a TMA + b_segment = True if segmentation if being done (or False if feature extraction only) + """ + if type=='exacloud': + os.chdir(f'{s_work_path}/exacloud/') + with open('TemplateExacloudCluster.java') as f: + s_file = f.read() + elif type=='eppec': + os.chdir(f'{s_work_path}/eppec/') + with open('TemplateEppecCluster.java') as f: + s_file = f.read() + else: + print('Error: type must be exacloud or eppec') + s_file = s_file.replace('PathtoImages',imagedir) + s_file = s_file.replace('PathtoSegmentation',f'{segmentdir}/{s_sample.split("-Scene")[0]}_Segmentation/') + s_file = s_file.replace('PathtoFeatures',f'{segmentdir}/{s_sample.split("-Scene")[0]}_Features/') + if b_segment: + s_file = s_file.replace('/*cif.Experiment','cif.Experiment') + s_file = s_file.replace('("Segmentation Done!") ;*/','("Segmentation Done!") ;') + if b_TMA: + s_file = s_file.replace('cif.CROPS ;','cif.TMA ;') + os.chdir(f'./{s_dir}/') + with open('Cluster.java', 'w') as f: + f.write(s_file) + +def registration_matlab(N_smpl='10000',N_colors='5',s_rootdir='PathtoImages',s_subdirname='RegisteredImages/',s_ref_id='./R1_*_c1_ORG.tif', + ls_order = ['R1','R2','R3','R4','R5','R6','R7','R8','R9','R10','R11','R0','R11Q']): + + """ + makes specific changes to template matlab scripts files in Jenny's directories to result in .m file + Input: + N_smpl = i_N_smpl; %number of features to detect in image (default = 10000) + N_colors = i_N_colors; %number of colors in R1 (default = 5) + ls_order = {RoundOrderString}; %list of names and order of rounds + s_rootdir = 'PathtoImages' %location of raw images in folder + s_ref_id = 'RefDapiUniqueID'; %shared unique identifier of reference dapi + s_subdirname = 'PathtoRegisteredImages' %location of folder where registered images will reside + """ + ls_order_q = [f"'{item}'" for item in ls_order] + #find template, open ,edit + os.chdir(f'{s_src_path}/src') + with open('template_registration_server_multislide_roundorder_scenes_2019_11_11.m') as f: + s_file = f.read() + s_file = s_file.replace('PathtoImages',s_rootdir) + s_file = s_file.replace('PathtoRegisteredImages',s_subdirname) + s_file = s_file.replace('i_N_smpl',N_smpl) + s_file = s_file.replace('i_N_colors',N_colors) + s_file = s_file.replace("RoundOrderString",",".join(ls_order_q)) + s_file = s_file.replace('RefDapiUniqueID',s_ref_id) + + #save edited .m file + os.chdir(s_rootdir) + with open('registration_py.m', 'w') as f: + f.write(s_file) + +def large_registration_matlab(N_smpl='10000',N_colors='5',s_rootdir='PathtoImages',s_subdirname='RegisteredImages',s_ref_id='./R1_*_c1_ORG.tif', + ls_order = ['R1','R2','R3','R4','R5','R6','R7','R8','R9','R10','R11','R0','R11Q'],d_crop_regions={1:'[0 0 1000 1000]'}): + """ + makes specific changes to template matlab scripts files in Jenny's directories to result in .m file + Input: + N_smpl = i_N_smpl; %number of features to detect in image (default = 10000) + N_colors = i_N_colors; %number of colors in R1 (default = 5) + ls_order = {RoundOrderString}; %list of names and order of rounds + s_rootdir = 'PathtoImages' %location of raw images in folder + s_ref_id = 'RefDapiUniqueID'; %shared unique identifier of reference dapi + s_subdirname = 'PathtoRegisteredImages' %location of folder where registered images will reside + d_crop_regions= dictioanr with crop integer as key, ans string with crop array as value e.g. {1:'[0 0 1000 1000]'} + + """ + ls_order_q = [f"'{item}'" for item in ls_order] + + os.chdir(f'{s_src_path}/src') + with open('template_registration_server_largeimages_roundorder_2019_11_11.m') as f: + s_file = f.read() + s_file = s_file.replace('PathtoImages',s_rootdir) + s_file = s_file.replace('PathtoRegisteredImages',s_subdirname) + s_file = s_file.replace('i_N_smpl',N_smpl) + s_file = s_file.replace('i_N_colors',N_colors) + s_file = s_file.replace("RoundOrderString",",".join(ls_order_q)) + s_file = s_file.replace('RefDapiUniqueID',s_ref_id) + + for i_crop_region, s_crop in d_crop_regions.items(): + s_file = s_file.replace(f'%{i_crop_region}%{i_crop_region}%','') + s_file = s_file.replace(f'[a_crop_{i_crop_region}]',s_crop) + #save edited .m file + os.chdir(s_rootdir) + with open('registration_py.m', 'w') as f: + f.write(s_file) + +def cmif_mkdir(ls_dir): + ''' + check if directories existe. if not, make them + ''' + for s_dir in ls_dir: + if not os.path.exists(s_dir): + os.makedirs(s_dir) + +######################### Old functions ############################ + +def check_reg_channels(ls_find=['c1_ORG','c2_ORG'], i_rows=2, t_figsize=(20,10), b_separate = False, b_mkdir=True): + """ + This script makes overviews of all the specified channel images of registered tiff images + in a big folder (slides prepared for segmentation for example) + Input: ls_find = list of channels to view + i_rows = number or rows in figure + t_figsize = (x, y) in inches size of figure + b_mkdir = boolean whether to make a new Check_Registration folder + Output: dictionary with {slide_color:number of rounds found} + images of all rounds of a certain slide_color + """ + d_result = {} + ls_error = [] + if b_separate: + s_dir = os.getcwd() + os.chdir('..') + s_path = os.getcwd() + if b_mkdir: + os.mkdir(f'./Check_Registration') + os.chdir(s_dir) + else: + s_path = os.getcwd() + if b_mkdir: + os.mkdir(f'./Check_Registration') + for s_find in ls_find: + #find all dapi slides + ls_dapis = [] + for s_dir in os.listdir(): + if s_dir.find(s_find) > -1: + ls_dapis = ls_dapis + [s_dir] + + #find all unique scenes + ls_scene_long = [] + for s_dapi in ls_dapis: + ls_scene_long = ls_scene_long + [(s_dapi.split('_')[2])] + ls_scene = list(set(ls_scene_long)) + ls_scene.sort() + + for s_scene in ls_scene: + print(f'Processing {s_scene}') + ls_dapi = [] + for s_file in ls_dapis: + if s_file.find(s_scene)>-1: + ls_dapi = ls_dapi + [s_file] + fig,ax = plt.subplots(i_rows,(len(ls_dapi)+(i_rows-1))//i_rows, figsize = t_figsize) + ax = ax.ravel() + ls_dapi.sort() + for x in range(len(ls_dapi)): + im_low = skimage.io.imread(ls_dapi[x]) + im = skimage.exposure.rescale_intensity(im_low,in_range=(np.quantile(im_low,0.02),np.quantile(im_low,0.98)+np.quantile(im_low,0.98)/2)) + ax[x].imshow(im, cmap='gray') + s_round = ls_dapi[x].split('_')[0].split('-')[1] + ax[x].set_title(s_round,{'fontsize':12}) + s_slide = ls_dapi[0].split('_')[2] + plt.tight_layout() + fig.savefig(f'{s_path}/Check_Registration/{s_slide}_{s_find}.png') + d_result.update({f'{s_slide}_{s_find}':len(ls_dapi)}) + ls_error = ls_error + [len(ls_dapi)] + if(len(set(ls_error))==1): + print("All checked scenes/channels have the same number of images") + else: + print("Warning: different number of images in some scenes/channels") + for s_key, i_item in d_result.items(): + print(f'{s_key} has {i_item} images') + return(d_result) + + +def check_names_deprecated(s_find='-Scene-001_c',b_print=False): + """ + Based on filenames in segment folder, + checks marker names against standard list of biomarkers + returns a dataframe with Rounds Cycles Info, and sets of wrong and correct names + Input: s_find = string that will be unique to one scene to check in the folder + """ + df_dapi = pd.DataFrame() #(columns=['rounds','colors','minimum','maximum','exposure','refexp','location']) + for s_name in sorted(os.listdir()): + if s_name.find(s_find) > -1: + s_color = s_name.split('_')[3] + if s_color != 'c1': + if b_print: + print(s_name) + if s_color == 'c2': + s_marker = s_name.split('_')[1].split('.')[0] + elif s_color == 'c3': + s_marker = s_name.split('_')[1].split('.')[1] + elif s_color == 'c4': + s_marker = s_name.split('_')[1].split('.')[2] + elif s_color == 'c5': + s_marker = s_name.split('_')[1].split('.')[3] + else: + print('Error: unrecognized channel name') + s_marker = 'error' + df_marker = pd.DataFrame(index = [s_marker],columns=['rounds','colors','minimum','maximum','exposure','refexp','location']) + df_marker.loc[s_marker,'rounds'] = s_name.split('_')[0].split('Registered-')[1] + df_marker.loc[s_marker,'colors'] = s_name.split('_')[3] + df_marker.loc[s_marker,'minimum'] = 1003 + df_marker.loc[s_marker,'maximum'] = 65535 + df_marker.loc[s_marker,'exposure'] = 100 + df_marker.loc[s_marker,'refexp'] = 100 + df_marker.loc[s_marker,'location'] = 'All' + df_dapi = df_dapi.append(df_marker) + es_names = set(df_dapi.index) + es_standard = {'PDL1','pERK','CK19','pHH3','CK14','Ki67','Ecad','PCNA','HER2','ER','CD44', + 'aSMA','AR','pAKT','LamAC','CK5','EGFR','pRB','FoxP3','CK7','PDPN','CD4','PgR','Vim', + 'CD8','CD31','CD45','panCK','CD68','PD1','CD20','CK8','cPARP','ColIV','ColI','CK17', + 'H3K4','gH2AX','CD3','H3K27','53BP1','BCL2','GRNZB','LamB1','pS6RP','BAX','RAD51', + 'R0c2','R0c3','R0c4','R0c5','R5Qc2','R5Qc3','R5Qc4','R5Qc5','R11Qc2','R11Qc3','R11Qc4','R11Qc5', + 'R7Qc2','R7Qc3','R7Qc4','R7Qc5','PDL1ab','PDL1d','R14Qc2','R14Qc3','R14Qc4','R14Qc5', + 'R8Qc2','R8Qc3','R8Qc4','R8Qc5','R12Qc2','R12Qc3','R12Qc4','R12Qc5','PgRc4', + 'Glut1','CoxIV','LamB2','S100','BMP4','BMP2','BMP6','pS62MYC', 'CGA', 'p63', 'SYP','PDGFRa', 'HIF1a'}#,'PDGFRB'CD66b (Neutrophils) HLA class II or CD21(Dendritic cells) + #BMP4 Fibronectin, CD11b (dendritic, macrophage/monocyte/granulocyte) CD163 (macrophages) + #CD83 (dendritic cells) FAP Muc1 + es_wrong = es_names - es_standard + es_right = es_standard.intersection(es_names) + print(f'Wrong names {es_wrong}') + print(f' Right names {es_right}') + return(df_dapi, es_wrong, es_right) + +def file_sort(s_sample, s_path, i_scenes=14,i_rounds=12,i_digits=3,ls_quench=['R5Q','R11Q'],s_find='_ORG.tif',b_scene=False): + ''' + count rounds and channels of images (koeis naming convention, not registered yet) + ''' + os.chdir(s_path) + se_dir = pd.Series(os.listdir()) + + se_dir = se_dir[se_dir.str.find(s_find)>-1] + se_dir = se_dir.sort_values() + se_dir = se_dir.reset_index() + se_dir = se_dir.drop('index',axis=1) + + print(s_sample) + print(f'Total _ORG.tif: {len(se_dir)}') + + #count files in each round, plus store file names on df_round + df_round = pd.DataFrame(index=range(540)) + i_grand_tot = 0 + for x in range(i_rounds): + se_round = se_dir[se_dir.iloc[:,0].str.contains(f'R{str(x)}_')] + se_round = se_round.rename({0:'round'},axis=1) + se_round = se_round.sort_values(by='round') + se_round = se_round.reset_index() + se_round = se_round.drop('index',axis=1) + i_tot = se_dir.iloc[:,0].str.contains(f'R{str(x)}_').sum() + i_round = 'Round ' + str(x) + print(f'{i_round}: {i_tot}') + i_grand_tot = i_grand_tot + i_tot + df_round[i_round]=se_round + df_round = df_round.dropna() + + #quenched round special loop + for s_quench in ls_quench: + #x = "{0:0>2}".format(x) + i_tot = se_dir.iloc[:,0].str.contains(s_quench).sum() + #i_round = 'Round ' + str(x) + print(f'{s_quench}: {i_tot}') + i_grand_tot = i_grand_tot + i_tot + print(f'Total files containing Rxx_: {i_grand_tot}') + + if b_scene: + #print number of files in each scene + for x in range(1,i_scenes+1): + if i_digits==3: + i_scene = "{0:0>3}".format(x) + elif i_digits==2: + i_scene = "{0:0>2}".format(x) + elif i_digits==1: + i_scene = "{0:0>1}".format(x) + else: + print('wrong i_digits input (must be between 1 and 3') + i_tot = se_dir.iloc[:,0].str.contains(f'Scene-{i_scene}_').sum() + i_round = 'Scene ' + str(x) + print(f'{i_round}: {i_tot}') + + #print number of files in each color + for x in range(1,6): + #i_scene = "{0:0>2}".format(x) + i_tot = se_dir.iloc[:,0].str.contains(f'_c{str(x)}_ORG').sum() + i_round = 'color ' + str(x) + print(f'{i_round}: {i_tot}') + + d_result = {} + for s_round in df_round.columns: + es_round = set([item.split('-Scene-')[1].split('_')[0] for item in list(df_round.loc[:,s_round].values)]) + d_result.update({s_round:es_round}) + print('\n') + + +def change_fname(s_old='_oldstring_',s_new='_newstring_',b_test=True): + """ + replace anything in file name + """ + if b_test: + ls_test = [] + for s_file in os.listdir(): + if s_file.find(s_old) > -1: + ls_test = ls_test + [s_file] + len(ls_test) + s_file_new = s_file.replace(s_old,s_new) + print(f'changed file {s_file}\tto {s_file_new}') + + print(f'total number of files changed is {len(ls_test)}') + #really rename + else: + ls_test = [] + for s_file in os.listdir(): + if s_file.find(s_old) > -1: + ls_test = ls_test + [s_file] + len(ls_test) + s_file_new = s_file.replace(s_old,s_new) + print(f'changed file {s_file}\tto {s_file_new}') + os.rename(s_file, s_file_new) #comment out this line to test + print(f'total number of files changed is {len(ls_test)}') + +def check_reg_slides(i_rows=2, t_figsize=(20,10), b_mkdir=True): + """ + This script makes overviews of all the dapi images of registered images in a big folder (slides prepared for segmentation for example) + """ + #find all dapi slides + ls_dapis = [] + for s_dir in os.listdir(): + if s_dir.find('c1_ORG') > -1: + ls_dapis = ls_dapis + [s_dir] + + #find all scenes + ls_scene_long = [] + for s_dapi in ls_dapis: + ls_scene_long = ls_scene_long + [(s_dapi.split('Scene')[1].split('_')[0])] + ls_scene = list(set(ls_scene_long)) + ls_scene.sort() + if b_mkdir: + os.mkdir(f'./Check_Registration') + for s_scene in ls_scene: + print(f'Processing {s_scene}') + ls_dapi = [] + for s_file in ls_dapis: + if s_file.find(f'Scene{s_scene}')>-1: + ls_dapi = ls_dapi + [s_file] + fig,ax = plt.subplots(i_rows,(len(ls_dapi)+(i_rows-1))//i_rows, figsize = t_figsize) + ax = ax.ravel() + ls_dapi.sort() + for x in range(len(ls_dapi)): + im_low = skimage.io.imread(ls_dapi[x]) + im = skimage.exposure.rescale_intensity(im_low,in_range=(np.quantile(im_low,0.02),np.quantile(im_low,0.98)+np.quantile(im_low,0.98)/2)) + ax[x].imshow(im, cmap='gray') + s_round = ls_dapi[x].split('_')[0].split('-')[1] + ax[x].set_title(s_round,{'fontsize':12}) + s_slide = ls_dapi[0].split('_')[2] + plt.tight_layout() + fig.savefig(f'Check_Registration/{s_slide}.png') + +def check_reg_dirs(s_dir='SlideName',s_subdir='Registered-SlideName', i_rows=2, t_figsize=(20,10), b_mkdir=True): + """ + this checks registration when files are in subdirectories (such as with large tissues, i.e. NP005) + """ + + rootdir = os.getcwd() + if b_mkdir: + os.mkdir(f'./Check_Registration') + #locate subdirectores + for s_dir in os.listdir(): + if s_dir.find(s_dir) > -1: + os.chdir(f'./{s_dir}') + + #locate registered image folders + for s_dir in os.listdir(): + #for s_dir in ls_test2: + if s_dir.find(s_subdir) > -1: #'Registered-BR1506-A019-Scene' + print(f'Processing {s_dir}') + ls_dapi = [] + os.chdir(f'./{s_dir}') + ls_file = os.listdir() + for s_file in ls_file: + if s_file.find('_c1_ORG.tif')>-1: + ls_dapi = ls_dapi + [s_file] + fig,ax = plt.subplots(i_rows,(len(ls_dapi)+(i_rows-1))//i_rows, figsize = (t_figsize)) #vertical + ax=ax.ravel() + ls_dapi.sort() + for x in range(len(ls_dapi)): + im_low = skimage.io.imread(ls_dapi[x]) + im = skimage.exposure.rescale_intensity(im_low,in_range=(np.quantile(im_low,0.02),np.quantile(im_low,0.98)+np.quantile(im_low,0.98)/2)) + ax[x].imshow(im, cmap='gray') + s_round = ls_dapi[x].split('_')[0].split('-')[1] + s_scene = ls_dapi[x].split('-Scene')[1].split('_')[0] + ax[x].set_title(f'{s_round} Scene{s_scene}',{'fontsize':12}) + plt.tight_layout() + + #save figure in the rootdir/Check_Registration folder + fig.savefig(f'{rootdir}/Check_Registration/{s_dir}.png') + #go out of the subfoler and start next processing + os.chdir('..') + +def test(name="this_is_you_name"): + ''' + This is my first doc string + ''' + print(f'hello {name}') + return True diff --git a/mplex_image/process.py b/mplex_image/process.py new file mode 100755 index 0000000..9057580 --- /dev/null +++ b/mplex_image/process.py @@ -0,0 +1,1208 @@ +#### +# title: process.py +# +# language: Python3.6 +# date: 2019-05-00 +# license: GPL>=v3 +# author: Jenny +# +# description: +# python3 library to process cyclic data and images after segmentation +#### + +#libraries +import pandas as pd +import matplotlib as mpl +mpl.use('agg') +import matplotlib.pyplot as plt +import os +import numpy as np +import skimage +import copy +import re +import seaborn as sns +from PIL import Image +Image.MAX_IMAGE_PIXELS = 1000000000 + +#function cellpose +def load_cellpose_df(ls_sample, segdir): + ''' + load all full feature dataframes in sample list + ''' + df_mi_full = pd.DataFrame() + for idx, s_sample in enumerate(ls_sample): + print(f'Loading features_{s_sample}_MeanIntensity_Centroid_Shape.csv') + df_tt = pd.read_csv(f'{segdir}/features_{s_sample}_MeanIntensity_Centroid_Shape.csv',index_col=0) + df_tt['slide'] = s_sample.split('-Scene')[0] + df_tt['scene'] = [item.split('_')[1] for item in df_tt.index] + df_mi_full = df_mi_full.append(df_tt,sort=True) + #add scene + df_mi_full['slide_scene'] = df_mi_full.slide + '_' + df_mi_full.scene + print('') + return(df_mi_full) + +# load li thresholds +def load_li(ls_sample, s_thresh, man_thresh): + ''' + load threshold on the segmentation marker images acquired during feature extraction + ''' + df_img_all =pd.DataFrame() + for s_sample in ls_sample: + print(f'Loading thresh_{s_sample}_ThresholdLi.csv') + df_img = pd.read_csv(f'thresh_{s_sample}_ThresholdLi.csv', index_col=0) + df_img['rounds'] = [item.split('_')[0].split('Registered-')[1] for item in df_img.index] + df_img['color'] = [item.split('_')[-2] for item in df_img.index] + df_img['slide'] = [item.split('_')[2].split('-Scene-')[0] for item in df_img.index] + df_img['scene'] = [item.split('_')[2].split('-Scene-')[1] for item in df_img.index] + df_img['slide_scene'] = df_img.slide + '_scene' + df_img.scene + #parse file name for biomarker + for s_index in df_img.index: + #print(s_index) + s_color = df_img.loc[s_index,'color'] + if s_color == 'c1': + s_marker = f"DAPI{df_img.loc[s_index,'rounds'].split('R')[1]}" + elif s_color == 'c2': + s_marker = s_index.split('_')[1].split('.')[0] + elif s_color == 'c3': + s_marker = s_index.split('_')[1].split('.')[1] + elif s_color == 'c4': + s_marker = s_index.split('_')[1].split('.')[2] + elif s_color == 'c5': + s_marker = s_index.split('_')[1].split('.')[3] + else: print('Error') + df_img.loc[s_index,'marker'] = s_marker + df_img_all = df_img_all.append(df_img) + print('') + #manually override too low Ecad thresh + if s_thresh !='': + df_img_all.loc[df_img_all[(df_img_all.marker==s_thresh) & (df_img_all.threshold_li < man_thresh)].index, 'threshold_li'] = man_thresh + return(df_img_all) + +def filter_cellpose_xy(df_mi_full,ls_centroid = ['DAPI2_nuclei_area', 'DAPI2_nuclei_centroid-0', 'DAPI2_nuclei_centroid-1','DAPI2_nuclei_eccentricity']): + ''' + select the nuclei centoids, area, eccentricity from a marker + default: use DAPI2 + ''' + #NOTE add area + df_xy = df_mi_full.loc[:,ls_centroid] + print('QC: make sure centroids dont have too many NAs') + print(df_xy.isna().sum()) + print('') + df_xy = df_xy.dropna(axis=0,how='any') + df_xy.columns = ['nuclei_area','DAPI_Y','DAPI_X','nuclei_eccentricity'] + df_xy['slide_scene'] = [item.split('_cell')[0] for item in df_xy.index] + return(df_xy) + +def drop_last_rounds(df_img_all,ls_filter,df_mi_full): + ''' + drop any rounds after the last round DAPI filter + ''' + df_img_all['round_ord'] = [re.sub('Q','.5', item) for item in df_img_all.rounds] + df_img_all['round_ord'] = [float(re.sub('[^0-9.]','', item)) for item in df_img_all.round_ord] + i_max = df_img_all[df_img_all.marker.isin([item.split('_')[0] for item in ls_filter])].sort_values('round_ord').iloc[-1].round_ord + print(f'Dropping markers after round {i_max}') + ls_drop_marker = [item + '_' for item in sorted(set(df_img_all[(df_img_all.round_ord>i_max)].marker))] + [print(item) for item in ls_drop_marker] + print('') + [df_mi_full.drop(df_mi_full.columns[df_mi_full.columns.str.contains(item)],axis=1,inplace=True) for item in ls_drop_marker] + return(df_mi_full,i_max) + +def plot_thresh(df_img_all,s_thresh): + ''' + tissues: plot threshold across all tissues + (negative scenes will drive down the mean + ''' + ls_slides = sorted(set(df_mi_full.slide)) + df_plot = df_img_all[(df_img_all.marker==s_thresh)].loc[:,['threshold_li']] + fig,ax=plt.subplots(figsize=(4,3.5)) + sns.stripplot(data=df_plot) + sns.barplot(data=df_plot, alpha=0.5) + labels = ax.get_xticklabels + plt.tight_layout() + fig.savefig(f'{qcdir}/QC_EcadThresh_{".".join(ls_slides)}.png') + +def fill_cellpose_nas(df_mi_full,ls_marker_cyto,s_thresh='Ecad',man_thresh=1000): + ''' + some nuclei don't have a cytoplasm, replace NA with perinuc5 + ''' + df = df_mi_full.copy(deep=True) + # since segmentation was run on ecad, use ecad threshold + print(f'Finding {s_thresh} positive cells') + ls_neg_cells = (df_mi_full[~(df_mi_full.loc[:,f'{s_thresh}_cytoplasm'] > man_thresh)]).index.tolist()#= ls_neg_cells + ls_neg_slide + print('') + # replace cells without cytoplasm (ecad) with perinuc 5 + print(f'For cells that are {s_thresh} negative:') + for s_marker in ls_marker_cyto: + print(f'Replace {s_marker}_cytoplasm nas') + df.loc[ls_neg_cells,f'{s_marker}_cytoplasm'] = df.loc[ls_neg_cells,f'{s_marker}_perinuc5'] + print(f'with {s_marker}_perinuc5') + df[f'{s_thresh}_negative'] = df.index.isin(ls_neg_cells) + return(df) + +def shrink_seg_regions(df_mi_full,s_thresh,ls_celline=[],ls_shrunk=[]): + ''' + For markers with stromal to tumor bleedthrough, use shrunken segmentation region + ''' + #enforce cell lines as tumor + print('') + if len(ls_celline) > 0: + print([f'Enforce {item} as tumor' for item in ls_celline]) + ls_ecad_cells = df_mi_full[~df_mi_full.loc[:,f'{s_thresh}_negative']].index + ls_tumor_cells = (df_mi_full[(df_mi_full.index.isin(ls_ecad_cells)) | (df_mi_full.slide_scene.isin(ls_celline))]).index + ls_stromal_cells = (df_mi_full[~df_mi_full.index.isin(ls_tumor_cells)]).index + #relplace tumor cell CD44 and Vim with shrunken area (only helps bleed trough a little) + print('For markers with stromal to tumor bleedthrough, use shrunken segmentation region:') + for s_marker in ls_shrunk: + print(f'Replace {s_marker.split("_")[0]}_perinuc5 in tumor cells with') + df_mi_full.loc[ls_tumor_cells,f'{s_marker.split("_")[0]}_perinuc5'] = df_mi_full.loc[ls_tumor_cells,f'{s_marker}'] + print(f'with {s_marker}') + print('') + return(df_mi_full) + +def fill_membrane_nas(df_mi_full, df_mi_mem,s_thresh='Ecad',ls_membrane=['HER2']): + ''' + fill cell membrane nsa with expanded nuclei nas + ''' + ls_neg = df_mi_full[(df_mi_full.loc[:,f'{s_thresh}_negative']) & (df_mi_full.index.isin(df_mi_mem.index))].index + ls_pos = df_mi_full[(~df_mi_full.loc[:,f'{s_thresh}_negative']) & (df_mi_full.index.isin(df_mi_mem.index))].index + for s_membrane in ls_membrane: + print(f'Replace {s_membrane}_cellmem25 nas \n with {s_membrane}_exp5nucmembrane25') + df_mi_mem.loc[ls_neg,f'{s_membrane}_cellmem25'] = df_mi_mem.loc[ls_neg,f'{s_membrane}_exp5nucmembrane25'] + ls_na = df_mi_mem.loc[df_mi_mem.loc[:,f'{s_membrane}_cellmem25'].isna(),:].index + df_mi_mem.loc[ls_na,f'{s_membrane}_cellmem25'] = df_mi_mem.loc[ls_na,f'{s_membrane}_exp5nucmembrane25'] + df_merge = df_mi_full.merge(df_mi_mem, left_index=True, right_index=True) + print('') + return(df_merge) + +def fill_bright_nas(ls_membrane,s_sample,s_thresh,df_mi_filled,segdir): + if len(ls_membrane) > 0: + print(f'Loading features_{s_sample}_BrightMeanIntensity.csv') + df_mi_mem = pd.read_csv(f'{segdir}/features_{s_sample}_BrightMeanIntensity.csv',index_col=0) + df_mi_mem_fill = fill_membrane_nas(df_mi_filled, df_mi_mem,s_thresh=s_thresh,ls_membrane=ls_membrane) + else: + df_mi_mem_fill = df_mi_filled + return(df_mi_mem_fill) + +def auto_threshold(df_mi,df_img_all): + # # Auto threshold + + #make positive dataframe to check threhsolds + ls_scene = sorted(set(df_mi.slide_scene)) + + df_pos_auto = pd.DataFrame() + d_thresh_record= {} + + for s_slide_scene in ls_scene: + print(f'Thresholding {s_slide_scene}') + ls_index = df_mi[df_mi.slide_scene==s_slide_scene].index + df_scene = pd.DataFrame(index=ls_index) + df_img_scene = df_img_all[df_img_all.slide_scene==s_slide_scene] + + for s_index in df_img_scene.index: + s_scene =f"{df_img_all.loc[s_index,'slide']}_scene{df_img_all.loc[s_index,'scene']}" + s_marker = df_img_all.loc[s_index,'marker'] + s_columns = df_mi.columns[df_mi.columns.str.contains(f"{s_marker}_")] + if len(s_columns)==1: + s_marker_loc = s_columns[0] + else: + continue + i_thresh = df_img_all.loc[s_index,'threshold_li'] + d_thresh_record.update({f'{s_scene}_{s_marker}':i_thresh}) + df_scene.loc[ls_index,s_marker_loc] = df_mi.loc[ls_index,s_marker_loc] >= i_thresh + df_pos_auto = df_pos_auto.append(df_scene) + return(df_pos_auto,d_thresh_record) + +def positive_scatterplots(df_pos_auto,d_thresh_record,df_xy,ls_color,qcdir='.'): + ''' + for marker in ls_color, plot positive cells location in tissue + ''' + ls_scene = sorted(set(df_xy.slide_scene)) + + for s_scene in ls_scene: + print(f'Plotting {s_scene}') + #negative cells = all cells even before dapi filtering + df_neg = df_xy[(df_xy.slide_scene==s_scene)] + #plot + fig, ax = plt.subplots(2, ((len(ls_color))+1)//2, figsize=(18,12)) #figsize=(18,12) + ax = ax.ravel() + for ax_num, s_color in enumerate(ls_color): + s_marker = s_color.split('_')[0] + s_min = d_thresh_record[f"{s_scene}_{s_marker}"] + #positive cells = positive cells based on threshold + ls_pos_index = (df_pos_auto[df_pos_auto.loc[:,s_color]]).index + df_color_pos = df_neg[df_neg.index.isin(ls_pos_index)] + if len(df_color_pos)>=1: + #plot negative cells + ax[ax_num].scatter(data=df_neg,x='DAPI_X',y='DAPI_Y',color='silver',s=1) + #plot positive cells + ax[ax_num].scatter(data=df_color_pos, x='DAPI_X',y='DAPI_Y',color='DarkBlue',s=.5) + + ax[ax_num].axis('equal') + ax[ax_num].set_ylim(ax[ax_num].get_ylim()[::-1]) + ax[ax_num].set_title(f'{s_marker} min={int(s_min)}') + else: + ax[ax_num].set_title(f'{s_marker} min={int(s_min)}') + ls_save = [item.split('_')[0] for item in ls_color] + fig.suptitle(s_scene) + fig.savefig(f'{qcdir}/QC_{".".join(ls_save)}_{s_scene}_auto.png') + +def plot_thresh_results(df_img_all,df_pos_auto,d_thresh_record,df_xy,i_max,s_thresh,qcdir): + ls_color = [item + '_nuclei' for item in df_img_all[(df_img_all.round_ord<=i_max) & (df_img_all.slide_scene==df_img_all.slide_scene.unique()[0]) & (df_img_all.marker.str.contains('DAPI'))].marker.tolist()] + positive_scatterplots(df_pos_auto,d_thresh_record,df_xy,ls_color + [f'{s_thresh}_cytoplasm'],qcdir) + return(ls_color) + +def filter_dapi_cellpose(df_pos_auto,ls_color,df_mi,ls_filter,qcdir='.'): + ''' + filter by cell positive for DAPI autotresholding, in rounds specified in ls_filter + error + ''' + #plot dapi thresholds + df_pos_auto['slide_scene'] = [item.split('_cell')[0] for item in df_pos_auto.index] + fig,ax=plt.subplots(figsize=(10,5)) + df_plot = df_pos_auto.loc[:,ls_color+['slide_scene']] + df_scenes = df_plot.groupby('slide_scene').sum().T/df_plot.groupby('slide_scene').sum().max(axis=1) + df_scenes.plot(ax=ax,colormap='tab20') + ax.set_xticks(np.arange(0,(len(df_scenes.index)),1)) #+1 + ax.set_xticklabels([item.split('_')[0] for item in df_scenes.index]) + ax.set_ylim(0.5,1.1) + ax.legend(loc=3) + plt.tight_layout() + df_pos_auto['slide'] = [item.split('_')[0] for item in df_pos_auto.index] + ls_slides = sorted(set(df_pos_auto.slide)) + fig.savefig(f'{qcdir}/QC_DAPIRounds_lineplot_{".".join(ls_slides)}.png') + #filter by first and last round dapi + ls_dapi_index = df_pos_auto[df_pos_auto.loc[:,ls_filter].all(axis=1)].index + #also filter by any dapi less than 1 in mean intensity + ls_dapi_missing = df_mi[(df_mi.loc[:,ls_color] < 1).sum(axis=1) > 0].index.tolist() + es_dapi_index = set(ls_dapi_index) - set(ls_dapi_missing) + print(f'number of cells before DAPI filter = {len(df_mi)}') + df_mi_filter = df_mi.loc[df_mi.index.isin(es_dapi_index),:] + [print(f'filtering by {item}') for item in ls_filter] + print(f'number of cells after DAPI filter = {len(df_mi_filter)}') + #drop cells with euler numer > 1 + # + # + return(df_mi_filter) + +def load_li_thresh(ls_sample, segdir): + # load li thresholds + os.chdir(segdir) + df_img_all =pd.DataFrame() + for s_sample in ls_sample: + df_img = pd.read_csv(f'thresh_{s_sample}_ThresholdLi.csv', index_col=0) + df_img['rounds'] = [item.split('_')[0].split('Registered-')[1] for item in df_img.index] + df_img['color'] = [item.split('_')[-2] for item in df_img.index] + df_img['slide'] = [item.split('_')[2].split('-Scene-')[0] for item in df_img.index] + df_img['scene'] = [item.split('_')[2].split('-Scene-')[1] for item in df_img.index] + df_img['slide_scene'] = df_img.slide + '_scene' + df_img.scene + #parse file name for biomarker + for s_index in df_img.index: + #print(s_index) + s_color = df_img.loc[s_index,'color'] + if s_color == 'c1': + s_marker = f"DAPI{df_img.loc[s_index,'rounds'].split('R')[1]}" + elif s_color == 'c2': + s_marker = s_index.split('_')[1].split('.')[0] + elif s_color == 'c3': + s_marker = s_index.split('_')[1].split('.')[1] + elif s_color == 'c4': + s_marker = s_index.split('_')[1].split('.')[2] + elif s_color == 'c5': + s_marker = s_index.split('_')[1].split('.')[3] + else: print('Error') + df_img.loc[s_index,'marker'] = s_marker + df_img_all = df_img_all.append(df_img) + return(df_img_all) + +def filter_standard(df_mi,d_channel,s_dapi): + """ + If biomarkers have standard names according to preprocess.check_names, + use the hard coded locations, adds any channels needed for af subtraction + Input: + df_mi= mean intensity dataframe + d_channel = dictionary of channel:background marker + """ + es_standard = {'PDL1_Ring','pERK_Nuclei','CK19_Ring','pHH3_Nuclei','CK14_Ring','Ki67_Nuclei','Ki67r_Nuclei','Ecad_Ring','PCNA_Nuclei','HER2_Ring','ER_Nuclei','CD44_Ring', + 'aSMA_Ring','AR_Nuclei','pAKT_Ring','LamAC_Nuclei','CK5_Ring','EGFR_Ring','pRb_Nuclei','FoxP3_Nuclei','CK7_Ring','PDPN_Ring','CD4_Ring','PgR_Nuclei','Vim_Ring', + 'CD8_Ring','CD31_Ring','CD45_Ring','panCK_Ring','CD68_Ring','PD1_Ring','CD20_Ring','CK8_Ring','cPARP_Nuclei','ColIV_Ring','ColI_Ring','CK17_Ring', + 'H3K4_Nuclei','gH2AX_Nuclei','CD3_Ring','H3K27_Nuclei','53BP1_Nuclei','BCL2_Ring','GRNZB_Nuclei','LamB1_Nuclei','pS6RP_Ring','BAX_Nuclei','RAD51_Nuclei', + 'Glut1_Ring','CoxIV_Ring','LamB2_Nuclei','S100_Ring','BMP4_Ring','PgRc4_Nuclei','pRB_Nuclei','p63_Nuclei','p63_Ring','CGA_Ring','SYP_Ring','pS62MYC_Nuclei', 'HIF1a_Nuclei', + 'PDGFRa_Ring', 'BMP2_Ring','PgRb_Nuclei','MUC1_Ring','CSF1R_Ring','CAV1_Ring','CCND1_Nuclei','CC3_Nuclei' } #PgRb is second PgR in dataset + #generate list of background markers needed for subtraction + lls_d_channel = [] + for s_key,ls_item in d_channel.items(): + lls_d_channel = lls_d_channel + [ls_item] + ls_background = [] + for ls_channel in lls_d_channel: + ls_background = ls_background + [f'{ls_channel[0]}_Ring'] + ls_background = ls_background + [f'{ls_channel[1]}_Nuclei'] + #ls_background.append(f'{s_dapi}_Nuclei') + ls_background.append(f'{s_dapi}') + se_background = set(ls_background) + es_common = set(df_mi.columns.tolist()).intersection(es_standard) | se_background + df_filtered_mi = df_mi.loc[:,sorted(es_common)] + return(df_filtered_mi, es_standard) + +def filter_loc_cellpose(df_mi_filled, ls_marker_cyto, ls_custom,filter_na=True): + ''' + get nuclei, perinuclei or cytoplasm, based on filter standard function + ''' + __ , es_standard = filter_standard(pd.DataFrame(columns=['filter_standard']),{},'filter_standard') + ls_marker = sorted(set([item.split('_')[0] for item in df_mi_filled.columns[(df_mi_filled.dtypes=='float64') & (~df_mi_filled.columns.str.contains('25'))]])) + if ls_marker.count('mean') != 0: + ls_marker.remove('mean') + es_marker = set(ls_marker) + se_stand = pd.Series(index=es_standard) + es_dapi = set([item.split('_')[0] for item in df_mi_filled.columns[df_mi_filled.columns.str.contains('DAPI')]]) + es_nuc = set([item.split('_')[0] for item in se_stand[se_stand.index.str.contains('_Nuclei')].index]) + es_nuc_select = es_nuc.intersection(es_marker) + print('Nuclear markers:') + print(es_nuc_select) + es_ring = set([item.split('_')[0] for item in se_stand[se_stand.index.str.contains('_Ring')].index]) + es_ring_select = es_ring.intersection(es_marker) + es_cyto = set(ls_marker_cyto) #set([item.split('_')[0] for item in ls_marker_cyto]) + es_ring_only = es_ring_select - es_cyto + print('Ring markers:') + print(es_ring_only) + print('Cytoplasm markers:') + print(es_cyto) + es_cust = set([item.split('_')[0] for item in ls_custom]) + es_left = es_marker - es_ring_only - es_cyto - es_nuc_select - es_dapi - es_cust + print('Custom markers:') + print(es_cust) + print('Markers with Nuclei or Cyto not specified: take both nuclei and ring') + print(es_left) + ls_n = [item + '_nuclei' for item in sorted(es_left | es_nuc_select | es_dapi)] + ls_pn = [item + '_perinuc5' for item in sorted(es_left | es_ring_only)] + ls_cyto = [item + '_cytoplasm' for item in sorted(es_cyto)] + ls_all = ls_custom + ls_pn + ls_cyto + ls_n + ['slide_scene'] + print(f'Missing {set(ls_all) - set(df_mi_filled.columns)}') + df_filter = df_mi_filled.loc[:,ls_all] + print('') + if filter_na: + print(f' NAs filtered: {len(df_filter) - len(df_filter.dropna())}') + df_filter = df_filter.dropna() + print('') + return(df_filter,es_standard) + +def marker_table(df_img_all,qcdir): + ''' + make a nice rounds/channels/markers table + ''' + df_img_all['round_ord'] = [re.sub('Q','.5', item) for item in df_img_all.rounds] + df_img_all['round_ord'] = [re.sub('r','.25', item) for item in df_img_all.round_ord] + df_img_all['round'] = [float(re.sub('[^0-9.]','', item)) for item in df_img_all.round_ord] + df_marker = df_img_all[(df_img_all.slide_scene==df_img_all.slide_scene.unique()[0])].loc[:,['marker','round','color']].pivot('round','color') + df_marker.index.name = None + df_marker.to_csv(f'{qcdir}/MarkerTable.csv',header=None) + +def filter_cellpose_df(s_sample,segdir,qcdir,s_thresh,ls_membrane,ls_marker_cyto,ls_custom,ls_filter,ls_shrunk,man_thresh = 900): + ''' + go from full dataframe and membrane dataframe to filtered datframe and xy coordinate dataframe + s_thresh='Ecad' + ls_membrane = ['HER2'] + ls_marker_cyto = ['CK14','CK5','CK17','CK19','CK7','CK8','Ecad','HER2','EGFR'] + ls_custom = ['HER2_cellmem25'] + ls_filter = ['DAPI9_nuclei','DAPI2_nuclei'] + ls_shrunk = ['CD44_nucadj2','Vim_nucadj2'] + man_thresh = 900 + ''' + # new + os.chdir(segdir) + df_img_all = load_li([s_sample],s_thresh, man_thresh) + df_mi_full = load_cellpose_df([s_sample], segdir) + df_xy = filter_cellpose_xy(df_mi_full) + df_mi_full, i_max = drop_last_rounds(df_img_all,ls_filter,df_mi_full) + df_mi_filled = fill_cellpose_nas(df_mi_full,ls_marker_cyto,s_thresh=s_thresh,man_thresh=man_thresh) + df_mi_filled = shrink_seg_regions(df_mi_filled,s_thresh,ls_celline=[],ls_shrunk=ls_shrunk) + df_mi_mem_fill = fill_bright_nas(ls_membrane,s_sample,s_thresh,df_mi_filled,segdir) + df_mi,es_standard = filter_loc_cellpose(df_mi_mem_fill, ls_marker_cyto, ls_custom) + df_pos_auto,d_thresh_record = auto_threshold(df_mi,df_img_all) + ls_color = plot_thresh_results(df_img_all,df_pos_auto,d_thresh_record,df_xy,i_max,s_thresh,qcdir) + df_mi_filter = filter_dapi_cellpose(df_pos_auto,ls_color,df_mi,ls_filter,qcdir) + df_mi_filter.to_csv(f'{segdir}/features_{s_sample}_FilteredMeanIntensity_{"_".join([item.split("_")[0] for item in ls_filter])}.csv') + df_xy.to_csv(f'{segdir}/features_{s_sample}_CentroidXY.csv') + return(df_mi_mem_fill,df_img_all) + +def filter_cellpose_background(df_mi_filled, es_standard): + ''' + given a set of standard biomarker subcellular locations, obtain the opposite subcellular location + and the mean intensity + input: df_mi = mean intensity dataframe with all biomarker locations + es_standard = biomarker ring or nuclei + return: dataframe with each scene and the quantiles of the negative cells scene + ''' + ls_rim = [item.replace('Nuclei','cytoplasm') for item in sorted(es_standard)] + ls_nuc_ring = [item.replace('Ring','nuclei') for item in ls_rim] + ls_nuc_ring.append('slide_scene') + ls_nuc_ring = sorted(set(df_mi_filled.columns).intersection(set(ls_nuc_ring))) + #quntiles + df_bg = df_mi_filled.loc[:,ls_nuc_ring].groupby('slide_scene').quantile(0) + df_bg.columns = [f'{item}' for item in df_bg.columns] + for q in np.arange(0,1,.1): + df_quantile = df_mi_filled.loc[:,ls_nuc_ring].groupby('slide_scene').quantile(q) + df_bg = df_bg.merge(df_quantile,left_index=True, right_index=True, suffixes=('',f'_{str(int(q*10))}')) + #drop duplicate + ls_nuc_ring.remove('slide_scene') + df_bg = df_bg.loc[:,~df_bg.columns.isin(ls_nuc_ring)] + return(df_bg) + +def filter_cellpose_df_old(df_mi_full): + ''' + old + ''' + #filter + ls_select = [ + #nuclei + 'DAPI1_nuclei', 'DAPI2_nuclei', 'DAPI3_nuclei', 'DAPI4_nuclei','DAPI5_nuclei', 'DAPI5Q_nuclei', + 'DAPI6_nuclei', 'DAPI7_nuclei','DAPI8_nuclei', 'DAPI9_nuclei', + 'DAPI10_nuclei', 'DAPI11_nuclei','DAPI12_nuclei','DAPI12Q_nuclei', + 'ER_nuclei','AR_nuclei','PgR_nuclei', + 'Ki67_nuclei', 'pRB_nuclei','PCNA_nuclei', 'pHH3_nuclei', + 'FoxP3_nuclei', 'GRNZB_nuclei', + 'H3K27_nuclei', 'H3K4_nuclei', + 'LamAC_nuclei', 'LamB1_nuclei', 'LamB2_nuclei', + 'HIF1a_nuclei', 'pERK_nuclei', 'cPARP_nuclei', 'gH2AX_nuclei', + + #perinuc5 + 'CD44_perinuc5', + 'CD20_perinuc5', 'CD31_perinuc5', + 'CD3_perinuc5', 'CD45_perinuc5', 'CD4_perinuc5', + 'CD68_perinuc5', 'CD8_perinuc5','pS6RP_perinuc5', + 'ColIV_perinuc5', 'ColI_perinuc5', 'CoxIV_perinuc5', + 'PD1_perinuc5', 'PDPN_perinuc5','PDGFRa_perinuc5', + 'Vim_perinuc5', 'aSMA_perinuc5','BMP2_perinuc5', + #cytoplasm + #'pAKT_cytoplasm','Glut1_cytoplasm', + 'CK14_cytoplasm','CK5_cytoplasm','CK17_cytoplasm', + 'CK19_cytoplasm','CK7_cytoplasm','CK8_cytoplasm', + 'Ecad_cytoplasm','HER2_cytoplasm','EGFR_cytoplasm', + #other + 'slide_scene', + #'area_segmented-nuclei', #'area_segmented-cells', + #'eccentricity_segmented-nuclei', #'eccentricity_segmented-cells', + #'mean_intensity_segmented-nuclei', #'mean_intensity_segmented-cells', + ] + + ls_negative = df_mi_full.columns[df_mi_full.columns.str.contains('_negative')].tolist() + #print(type(ls_negative)) + ls_select = ls_select + ls_negative + + df_mi_nas = df_mi_full.loc[:,df_mi_full.columns.isin(ls_select)] + print(f'Selected makers that were missing from mean intensity {set(ls_select) - set(df_mi_nas.columns)}') + #fiter out nas + print(f'Number on df_mi nas = {df_mi_nas.isna().sum().max()}') + df_mi = df_mi_nas.dropna(axis=0,how='any') + return(df_mi,df_mi_nas) + +###### below: functions for guillaumes features ######## + +def load_mi(s_sample, s_path='./', b_set_index=True): + """ + input: + s_sample: string with sample name + s_path: file path to data, default is current folder + b_set_index: + + output: + df_mi: dateframe with mean intensity + each row is a cell, each column is a biomarker_location + + description: + load the mean intensity dataframe + """ + print(f'features_{s_sample}_MeanIntensity.tsv') + df_mi = pd.read_csv( + f'{s_path}features_{s_sample}_MeanIntensity.tsv', + sep='\t', + index_col=0 + ) + if b_set_index: + df_mi = df_mi.set_index(f'{s_sample}_' + df_mi.index.astype(str)) + return(df_mi) + +def load_xy(s_sample, s_path='./', b_set_index=True): + """ + input: + s_sample: string with sample name + s_path: file path to data, default is current folder + b_set_index: + + output: + df_mi: dateframe with mean intensity + each row is a cell, each column is a biomarker_location + + description: + load the mean intensity dataframe + """ + print(f'features_{s_sample}_CentroidY.tsv') + df_y = pd.read_csv( + f'features_{s_sample}_CentroidY.tsv', + sep='\t', + index_col=0 + ) + if b_set_index: + df_y = df_y.set_index(f'{s_sample}_' + df_y.index.astype(str)) + + print(f'features_{s_sample}_CentroidX.tsv') + df_x = pd.read_csv( + f'features_{s_sample}_CentroidX.tsv', + sep='\t', + index_col=0 + ) + if b_set_index: + df_x = df_x.set_index(f'{s_sample}_' + df_x.index.astype(str)) + #merge the x and y dataframes + df_xy = pd.merge(df_x,df_y,left_index=True,right_index=True,suffixes=('_X', '_Y')) + return(df_xy) + +def add_scene(df,i_scene_index=1,s_group='scene'): + """ + decription: add a coulmn with a grouping to dataframe that has grouping in the index + """ + lst = df.index.str.split('_') + lst2 = [item[i_scene_index] for item in lst] + df[s_group] = lst2 + return(df) + +def filter_dapi(df_mi,df_xy,s_dapi='DAPI11_Nuclei',dapi_thresh=1000,b_images=False,t_figsize=(8,8)): + """ + description: return a dataframe where all cells have DAPI brigter than a threshold + right now the plotting works! + """ + df_filtered_mi = df_mi.copy(deep=True) + #get tissue id from the dataframe + s_tissue = df_mi.index[0].split('_')[0] + #DAPI filter + df_filtered_mi = df_filtered_mi[df_filtered_mi.loc[:,s_dapi]>dapi_thresh] + print(f'Cells before DAPI filter = {len(df_mi)}') + print(f'Cells after DAPI filter = {len(df_filtered_mi)}') + df_filtered_mi.index.name='UNIQID' + if b_images: + ls_scene=list(set(df_xy.scene)) + ls_scene.sort() + for s_scene in ls_scene: + df_pos = df_xy.loc[df_filtered_mi.index.tolist()] + df_pos_scene = df_pos[df_pos.scene==s_scene] + if len(df_pos_scene) >= 1: + fig,ax=plt.subplots(figsize=t_figsize) + ax.scatter(x=df_xy[df_xy.scene==s_scene].loc[:,'DAPI_X'], y=df_xy[df_xy.scene==s_scene].loc[:,'DAPI_Y'], color='silver',label='DAPI neg', s=2) + ax.scatter(x=df_pos_scene.loc[:,'DAPI_X'], y=df_pos_scene.loc[:,'DAPI_Y'], color='DarkBlue',label='DAPI pos',s=2) + ax.axis('equal') + ax.set_ylim(ax.get_ylim()[::-1]) + ax.set_title(f'{s_scene}_DAPI') + plt.legend(markerscale=3) + fig.savefig(f'{s_tissue}_{s_scene}_{s_dapi}{dapi_thresh}.png') + return(df_filtered_mi) + +def load_meta(s_sample, s_path='./',type='csv'): + """ + load rounds cycles table + make sure to specify location for use with downstream functions + make sure to add rows for any biomarkers used for analysis or processing + """ + #tab or space delimited + if type == 'Location': + print(f'metadata_{s_sample}_RoundsCyclesTable_location.txt') + df_t = pd.read_csv( + f'metadata_{s_sample}_RoundsCyclesTable_location.txt', + delim_whitespace=True, + header=None, + index_col=False, + names=['marker', 'rounds','color','minimum', 'maximum', 'exposure', 'refexp','location'], + ) + df_t = df_t.set_index(f'{s_sample}_' + df_t.index.astype(str)) + df_t.replace({'Nucleus':'Nuclei'},inplace=True) + df_t['marker_loc'] = df_t.marker + '_' + df_t.location + df_t.set_index(keys='marker_loc',inplace=True) + elif type == 'csv': + print(f'metadata_{s_sample}_RoundsCyclesTable.csv') + df_t = pd.read_csv( + f'metadata_{s_sample}_RoundsCyclesTable.csv', + header=0, + index_col=0, + names=['rounds','color','minimum', 'maximum', 'exposure', 'refexp','location'],#'marker', + ) + #df_t = df_t.set_index(f'{s_sample}_' + df_t.index.astype(str)) + df_t.replace({'Nucleus':'Nuclei'},inplace=True) + # + elif type == 'LocationCsv': + print(f'metadata_{s_sample}_RoundsCyclesTable_location.csv') + df_t = pd.read_csv( + f'metadata_{s_sample}_RoundsCyclesTable_location.csv', + header=0, + index_col=False, + names=['marker', 'rounds','color','minimum', 'maximum', 'exposure', 'refexp','location'], + ) + df_t = df_t.set_index(f'{s_sample}_' + df_t.index.astype(str)) + df_t.replace({'Nucleus':'Nuclei'},inplace=True) + df_t['marker_loc'] = df_t.marker + '_' + df_t.location + df_t.set_index(keys='marker_loc',inplace=True) + else: + print(f'metadata_{s_sample}_RoundsCyclesTable.txt') + df_t = pd.read_csv( + f'metadata_{s_sample}_RoundsCyclesTable.txt', + delim_whitespace=True, + header=None, + index_col=False, + names=['rounds','color','minimum', 'maximum', 'exposure', 'refexp','location'],#'marker', + ) + df_t = df_t.set_index(f'{s_sample}_' + df_t.index.astype(str)) + df_t.replace({'Nucleus':'Nuclei'},inplace=True) + return(df_t) + +def add_exposure_roundscyles(df_tc, df_expc,es_standard,ls_dapi = ['DAPI12_Nuclei']): + """ + df_exp = dataframe of exposure times with columns [0, 1,2,3,4] + and index with czi image names + df_t = metadata with dataframe with ['marker','exposure'] + """ + df_t = copy.copy(df_tc) + df_exp = copy.copy(df_expc) + df_t['location'] = '' + df_t.drop([item.split('_')[0] for item in ls_dapi], inplace=True) + df_exp.columns = ['c' + str(int(item)+1) for item in df_exp.columns] + df_exp['rounds'] = [item.split('_')[0] for item in df_exp.index] + for s_index in df_t.index: + s_channel = df_t.loc[s_index,'colors'] + s_round = df_t.loc[s_index, 'rounds'] + print(s_round) + #look up exposure time for marker in metadata + df_t_image = df_exp[(df_exp.rounds==s_round)] + if len(df_t_image) > 0: + i_exposure = df_t_image.loc[:,s_channel] + df_t.loc[s_index,'exposure'] = i_exposure[0] + df_t.loc[s_index,'refexp'] = i_exposure[0] + else: + print(f'{s_marker} has no recorded exposure time') + s_ring = s_index + '_Ring' + s_nuc = s_index + '_Nuclei' + ls_loc = sorted(es_standard.intersection({s_ring,s_nuc})) + if len(ls_loc) == 1: + df_t.loc[s_index,'location'] = ls_loc[0].split('_')[1] + return(df_t) + +def filter_loc(df_mi,df_t): + """ + filters columns of dataframe based on locations selected in metadata_location table + """ + ls_bio_loc = df_t.index.tolist() + df_filtered_mi = df_mi.loc[:,ls_bio_loc] + return(df_filtered_mi) + +#R0c2 R0c3 R0c4 R0c5 panCK CK14 Ki67 CK19 R1rc2 R1rc3 Ki67r R1rc5 PCNA HER2 ER Ecad aSMA AR pAKT +#CD44 CK5 EGFR pRB LamAC pHH3 PDPN pERK FoxP3 R5Qc2 R5Qc3 R5Qc4 R5Qc5 CK7 CD68 PD1 CD45 Vim CD8 CD4 PgR CK8 cPARP ColIV CD20 CK17 +#H3K4 gH2AX ColI H3K27 pS6RP CD31 GRNZB LamB1 CoxIV HIF1a CD3 Glut1 PDGFRa LamB2 BMP2 R12Qc2 R12Qc3 R12Qc4 R12Qc5 DAPI12 + +def filter_background(df_mi, es_standard): + ''' + given a set of standard biomarker subcellular locations, obtain the opposite subcellular location + and the mean intensity + input: df_mi = mean intensity dataframe with all biomarker locations + es_standard = biomarker ring or nuclei + return: dataframe with each scene and the quantiles of the negative cells + ''' + ls_rim = [item.replace('Nuclei','Rim') for item in sorted(es_standard)] + ls_nuc_rim = [item.replace('Ring','Nuclei') for item in ls_rim] + ls_nuc_ring = [item.replace('Rim','Ring') for item in ls_nuc_rim] + ls_nuc_ring.append('scene') + ls_nuc_rim.append('scene') + df_scene = add_scene(df_mi) + ls_nuc_ring = sorted(set(df_scene.columns).intersection(set(ls_nuc_ring))) + #quntiles + df_bg = df_scene.loc[:,ls_nuc_ring].groupby('scene').quantile(0) + df_bg.columns = [f'{item}' for item in df_bg.columns] + for q in np.arange(0,1,.1): + df_quantile = df_scene.loc[:,ls_nuc_ring].groupby('scene').quantile(q) + df_bg = df_bg.merge(df_quantile,left_index=True, right_index=True, suffixes=('',f'_{str(int(q*10))}')) + print(q) + print(f'_{str(int(q*10))}') + #mean + df_quantile = df_scene.loc[:,ls_nuc_ring].groupby('scene').mean() + df_bg = df_bg.merge(df_quantile,left_index=True, right_index=True, suffixes=('','_mean')) + #drop duplicate + ls_nuc_ring.remove('scene') + df_bg = df_bg.loc[:,~df_bg.columns.isin(ls_nuc_ring)] + return(df_bg) + +def exposure_norm(df_mi,df_t,d_factor={'c1':10,'c2':30,'c3':200,'c4':500,'c5':500}): + """ + normalizes to standard exposure times + input: mean intensity, and metadata table with exposure time + """ + df_norm = pd.DataFrame() + ls_columns = [item.split('_')[0] for item in df_mi.columns.tolist()] + ls_column_mi = df_mi.columns.tolist() + for idx, s_column in enumerate(ls_columns): + + s_marker = s_column.split('_')[0] + i_exp = df_t.loc[s_column,'exposure'] + print(f'Processing exposure time for {s_column}: {i_exp}') + print(f'Processing mean intensity {ls_column_mi[idx]}') + i_factor = d_factor[df_t.loc[s_column,'colors']] + se_exp = df_mi.loc[:,ls_column_mi[idx]] + df_norm[ls_column_mi[idx]] = se_exp/i_exp*i_factor + return(df_norm) + +def af_subtract(df_norm,df_t,d_channel={'c2':['L488','L488'],'c3':['L555','L555'],'c4':['L647','L647'],'c5':['L750','L750']},ls_exclude=[]): + """ + given an exposure normalized dataframe, metadata with biomarker location, and a dictionary of background channels, subtracts + correct background intensity from each cell + input: + d_channel = dictionary, key is color i.e. 'c2', value is list of ['Ring','Nuclei'] + ls_exclude = markers to not subtract + output: + df_mi_sub,ls_sub,ls_record + """ + #generate list of background markers needed for subtraction + lls_d_channel = [] + for s_key,ls_item in d_channel.items(): + lls_d_channel = lls_d_channel + [ls_item] + ls_background = [] + for ls_channel in lls_d_channel: + ls_background = ls_background + [f'{ls_channel[0]}_Ring'] + ls_background = ls_background + [f'{ls_channel[1]}_Nuclei'] + se_background = set(ls_background) + se_exclude = set([item + '_Ring' for item in ls_exclude] + [item + '_Nuclei' for item in ls_exclude]).intersection(set(df_norm.columns.tolist())) + se_all = set(df_norm.columns.tolist()) + se_sub = se_all - se_background - se_exclude + ls_sub = list(se_sub) + + #subtract AF channels + df_mi_sub = pd.DataFrame() + + ls_record = [] + for s_marker_loc in ls_sub: + print(s_marker_loc) + s_marker = s_marker_loc.split('_')[0] + s_loc = s_marker_loc.split('_')[1] + s_channel = df_t.loc[s_marker,'colors'] + if s_channel == 'c1': + df_mi_sub[s_marker_loc] = df_norm.loc[:,s_marker_loc] + continue + if s_loc =='Nuclei': + s_AF = d_channel[s_channel][1] + elif s_loc == 'Ring': + s_AF = d_channel[s_channel][0] + else: + print('Error: location must be Ring or Nucleus') + s_AF_loc = s_AF + '_' + s_loc + df_mi_sub[s_marker_loc] = df_norm.loc[:,s_marker_loc] - df_norm.loc[:,s_AF_loc] + print(f'From {s_marker_loc} subtracting {s_AF_loc}') + ls_record = ls_record + [f'From {s_marker_loc} subtracting {s_AF_loc}\n'] + for s_marker in sorted(se_exclude): + ls_record = ls_record + [f'From {s_marker} subtracting None\n'] + df_mi_sub[sorted(se_exclude)] = df_norm.loc[:,sorted(se_exclude)] + #f = open(f"AFsubtractionData.txt", "w") + #f.writelines(ls_record) + #f.close() + #error check + print('AF subtraction not performed for the following markers:') + print(set(df_t.index) - set(ls_sub)) + + return(df_mi_sub,ls_sub,ls_record) + +def plot_subtraction(df_norm,df_sub,ls_scene=None): + """ + makes scatterplots of each marker, subtracted versus original meanintensity per cell, to judge subtraction effectiveness + """ + if ls_scene == None: + ls_scene = list(set(df_norm.scene)) + ls_marker = df_sub.columns.tolist() + ls_marker.remove('scene') + ls_scene.sort() + for s_marker in ls_marker: + print(f'Plotting {s_marker}') + fig, ax = plt.subplots(2,(len(ls_scene)+1)//2, figsize = (12,4)) + ax = ax.ravel() + ax_num = -1 + for s_scene in ls_scene: + df_subtracted = df_sub[df_sub.scene==s_scene] + df_original = df_norm[df_norm.scene==s_scene] + ax_num = ax_num + 1 + ax[ax_num].scatter(x=df_original.loc[:,s_marker],y=df_subtracted.loc[:,s_marker],s=1,alpha=0.8) + ax[ax_num].set_title(s_scene,{'fontsize': 10,'verticalalignment': 'center'}) + fig.text(0.5, 0.01, s_marker, ha='center') + fig.text(0.6, 0.01, 'Original', ha='center') + fig.text(0.01, 0.6, 'Subtracted', va='center', rotation='vertical') + plt.tight_layout() + fig.savefig(f'{s_marker}_NegativevsOriginal.png') + +def output_subtract(df_sub,df_t,d_factor={'c1':10,'c2':30,'c3':200,'c4':500,'c5':500}): + """ + this un-normalizes by exposure time to output a new dataframe of AF subtracted cells for analysis + """ + ls_sub = df_sub.columns.tolist() + result = any(elem == 'scene' for elem in ls_sub) + if result: + ls_sub.remove('scene') + df_sub = df_sub.drop(columns='scene') + else: + print('no scene column') + df_mi_zero = df_sub.clip(lower = 0) + df_mi_factor = pd.DataFrame() + for s_sub in ls_sub: + s_dft_index = s_sub.split('_')[0] + i_reverse_factor = df_t.loc[s_dft_index,'exposure']/d_factor[df_t.loc[s_dft_index,'colors']] + df_mi_factor[s_sub] = df_mi_zero.loc[:,s_sub]*i_reverse_factor + return df_mi_factor + +def af_subtract_images(df_t,d_channel={'c2':['L488','L488'],'c3':['L555','L555'],'c4':['L647','L647'],'c5':['L750','L750']},s_dapi='DAPI11_Nuclei',b_mkdir=True): + """ + This code loads 16 bit grayscale tiffs, performs AF subtraction of channels/rounds defined by the user, and outputs 8 bit AF subtracted tiffs for visualization. + The data required is: + 1. The RoundsCyclesTable.txt with the location (Nucleus/Ring) specified (not All), and real expsure times + 2. 16 bit grayscale tiff images following Koei's naming convention (script processes the list of folders ls_folder) + Note: name of folder can be anything + """ + #generate list of markers needing subtraction + lls_d_channel = [] + for s_key in d_channel: + lls_d_channel = lls_d_channel + [d_channel[s_key]] + ls_background = [] + for ls_channel in lls_d_channel: + ls_background = ls_background + [f'{ls_channel[0]}_Ring'] + ls_background = ls_background + [f'{ls_channel[1]}_Nuclei'] + se_background = set(ls_background) + se_all = set(df_t.index) + se_sub = se_all - se_background + ls_sub = list(se_sub) + #ls_sub.remove(s_dapi) #don't need line if s_DAPI is c1 + #subtract images + #os.makedirs('8bit/', exist_ok=True) + if b_mkdir: + os.mkdir('8bit') + ls_image = os.listdir() + ls_slide = [] + ls_image_org = [] + for s_image in ls_image: + if s_image.find('_ORG.tif')>-1: + #make a list of slides/scenes in the folder + s_slide = s_image.split('_')[2] + ls_slide = ls_slide + [s_slide] + #make a list of all original images in the folder + ls_image_org = ls_image_org + [s_image] + ls_slide = list(set(ls_slide)) + #process each slide in the folder + for s_slide in ls_slide: + print(f'Processing {s_slide}') + df_t['image'] = 'NA' + ls_dapi = [] + + for s_image in ls_image_org: + + #grab all original images with slide/scene name + if s_image.find(s_slide) > -1: + + #add matching image name to df_t (fore specific slide/scene, dapi not included) + s_round = s_image.split('Registered-')[1].split('_')[0] + s_color = s_image.split('Scene-')[1].split('_')[1] + s_index = df_t[(df_t.rounds==s_round) & (df_t.color==s_color)].index + df_t.loc[s_index,'image'] = s_image + if s_color == 'c1': + ls_dapi = ls_dapi + [s_image] + #subtract images + ls_record = [] + for s_marker_loc in ls_sub: + s_marker = s_marker_loc.split('_')[0] + s_loc = s_marker_loc.split('_')[1] + s_rounds= df_t.loc[s_marker_loc,'rounds'] + s_channel = df_t.loc[s_marker_loc,'color'] + if s_channel == 'c1': + print(f'{s_marker_loc} is DAPI') + continue + elif s_loc =='Nuclei': + s_AF = d_channel[s_channel][1] + elif s_loc == 'Ring': + s_AF = d_channel[s_channel][0] + else: + print('Error: location must be Ring or Nucleus') + s_AF_loc = s_AF + '_' + s_loc + print(f'From {s_marker_loc} subtracting {s_AF_loc}') + s_image = df_t.loc[s_marker_loc,'image'] + s_background = df_t.loc[s_AF_loc,'image'] + a_img = skimage.io.imread(s_image) + a_AF = skimage.io.imread(s_background) + #divide each image by exposure time + #subtract 1 ms AF from 1 ms signal + #multiply by original image exposure time + a_sub = (a_img/df_t.loc[s_marker_loc,'exposure'] - a_AF/df_t.loc[s_AF_loc,'exposure'])*df_t.loc[s_marker_loc,'exposure'] + + ls_record = ls_record + [f'From {s_marker_loc} subtracting {s_AF_loc}\n'] + #make all negative numbers into zero + a_zero = a_sub.clip(min=0,max=a_sub.max()) + a_zero_8bit = (a_zero/256).astype(np.uint8) + s_fname = f"8bit/{s_rounds}_{s_marker}_{s_slide}_{s_channel}_8bit.tif" + skimage.io.imsave(s_fname,a_zero_8bit) + f = open(f"8bit/AFsubtractionImages.txt", "w") + f.writelines(ls_record) + f.close() + #save 8 bit dapis + for s_dapi in ls_dapi: + a_img = skimage.io.imread(s_dapi) + a_zero_8bit = (a_img/256).astype(np.uint8) + s_marker = 'DAPI' + s_channel = 'c1' + s_round = s_dapi.split('Registered-')[1].split('_')[0] + s_fname = f"8bit/{s_round}_{s_marker}_{s_slide}_{s_channel}_8bit.tif" + skimage.io.imsave(s_fname,a_zero_8bit) + +def round_overlays(): + """ + output multipage tiffs with five channels per round + """ + os.chdir('./8bit') + ls_image = os.listdir() + ls_slide = [] + ls_image_org = [] + ls_round = [] + + for s_image in ls_image: + if s_image.find('8bit.tif') > -1: + #make a list of slides/scenes + #also make list of rounds + s_slide = s_image.split('_')[2] + ls_slide = ls_slide + [s_slide] + ls_image_org = ls_image_org + [s_image] + s_round = s_image.split('_')[0] + ls_round = ls_round + [s_round] + ls_slide = list(set(ls_slide)) + ls_round = list(set(ls_round)) + for s_slide in ls_slide: + print(f'Processing {s_slide}') + for s_round in ls_round: + d_overlay = {} + ls_color_round = [] + for s_image in ls_image_org: + if s_image.find(s_slide) > -1: + if s_image.find(f'{s_round}_') == 0: + s_color = s_image.split('_')[3] + d_overlay.update({s_color:s_image}) + s_image_round = s_image + a_size = skimage.io.imread(s_image_round) + a_overlay = np.zeros((len(d_overlay),a_size.shape[0],a_size.shape[1]),dtype=np.uint8) + s_biomarker_all = '' + i = -1 + for s_color in sorted(d_overlay.keys()): + i = i + 1 + s_overlay= d_overlay[s_color] + s_biomarker = s_overlay.split('_')[1] + '.' + s_biomarker_all = s_biomarker_all + s_biomarker + a_channel = skimage.io.imread(s_overlay) + a_overlay[i,:,:] = a_channel + s_biomarker_all = s_biomarker_all[:-1] + #this works. Open in image j. use Image/Color/Make Composite. Then use + #Image/Color/Channels Tool to turn on and off channels + #use Image/Adjust/Brightness/Contrast to adjust + with skimage.external.tifffile.TiffWriter(f'{s_round}_{s_biomarker_all}_{s_slide}_overlay.tiff', imagej=True) as tif: + for i in range(a_overlay.shape[0]): + tif.save(a_overlay[i]) + os.chdir('..') + +def custom_overlays(d_combos, df_img, df_dapi): + """ + output custon multi page tiffs according to dictionary, with s_dapi as channel 1 in each overlay + BUG with 53BP1 + d_combos = {'Immune':{'CD45', 'PD1', 'CD8', 'CD4', 'CD68', 'FoxP3','GRNZB','CD20','CD3'}, + 'Stromal':{'Vim', 'aSMA', 'PDPN', 'CD31', 'ColIV','ColI'}, + 'Differentiation':{'CK19', 'CK7','CK5', 'CK14', 'CK17','CK8'}, + 'Tumor':{'HER2', 'Ecad', 'ER', 'PgR','Ki67','PCNA'}, + 'Proliferation':{'EGFR','CD44','AR','pHH3','pRB'}, + 'Functional':{'pS6RP','H3K27','H3K4','cPARP','gH2AX','pAKT','pERK'}, + 'Lamins':{'LamB1','LamAC', 'LamB2'}} + """ + #os.chdir('./AFSubtracted') + + ls_slide = list(set(df_img.scene)) + #now make overlays + for s_slide in ls_slide: + print(f'Processing {s_slide}') + df_slide = df_img[df_img.scene==s_slide] + s_image_round = (df_dapi[df_dapi.scene == s_slide]).index[0] + if len((df_dapi[df_dapi.scene == s_slide]).index) == 0: + print('Error: dapi not found') + elif len((df_dapi[df_dapi.scene == s_slide]).index) > 1: + print('Error: too many dapi images found') + else: + print(s_image_round) + #exclude any missing biomarkers + es_all = set(df_slide.marker) + if len(list(set(df_img.imagetype)))==1: + s_imagetype = list(set(df_img.imagetype))[0] + print(s_imagetype) + else: + print('Error: more than one image type)') + for s_type in d_combos: + d_overlay = {} + es_combos = d_combos[s_type] + es_combos_shared = es_combos.intersection(es_all) + for idx, s_combo in enumerate(sorted(es_combos_shared)): + s_filename = (df_slide[df_slide.marker==s_combo]).index[0] + if len((df_slide[df_slide.marker==s_combo]).index) == 0: + print('Error: marker not found') + elif len((df_slide[df_slide.marker==s_combo]).index) > 1: + print('Error: too many marker images found') + else: + print(s_filename) + d_overlay.update({s_combo:s_filename}) + d_overlay.update({'1AAADAPI':s_image_round}) + a_size = skimage.io.imread(s_image_round) + a_overlay = np.zeros((len(d_overlay),a_size.shape[0],a_size.shape[1]),dtype=np.uint8) + s_biomarker_all = '' + i = -1 + for s_color in sorted(d_overlay.keys()): + i = i + 1 + s_overlay= d_overlay[s_color] + s_biomarker = s_color.split('1AAA')[0] + '.' + s_biomarker_all = s_biomarker_all + s_biomarker + a_channel = skimage.io.imread(s_overlay) + if s_imagetype=='ORG': + a_channel = (a_channel/256).astype(np.uint8) + print('covert to 8 bit') + a_overlay[i,:,:] = a_channel + s_biomarker_all = s_biomarker_all[1:-1] + #this works. Open in image j. use Image/Color/Make Composite. Then use + #Image/Color/Channels Tool to turn on and off channels + #use Image/Adjust/Brightness/Contrast to adjust + with skimage.external.tifffile.TiffWriter(f'./{s_type}_{((df_dapi[df_dapi.scene==s_slide]).marker[0])}.{s_biomarker_all}_{s_slide}_overlay.tiff', imagej=True) as tif: + for i in range(a_overlay.shape[0]): + tif.save(a_overlay[i]) + print(f'saved {s_type}') + +def custom_crop_overlays(d_combos,d_crop, df_img,s_dapi, tu_dim=(1000,1000)): #df_dapi, + """ + output custon multi page tiffs according to dictionary, with s_dapi as channel 1 in each overlay + BUG with 53BP1 + d_crop : {slide_scene : (x,y) coord + tu_dim = (width, height) + d_combos = {'Immune':{'CD45', 'PD1', 'CD8', 'CD4', 'CD68', 'FoxP3','GRNZB','CD20','CD3'}, + 'Stromal':{'Vim', 'aSMA', 'PDPN', 'CD31', 'ColIV','ColI'}, + 'Differentiation':{'CK19', 'CK7','CK5', 'CK14', 'CK17','CK8'}, + 'Tumor':{'HER2', 'Ecad', 'ER', 'PgR','Ki67','PCNA'}, + 'Proliferation':{'EGFR','CD44','AR','pHH3','pRB'}, + 'Functional':{'pS6RP','H3K27','H3K4','cPARP','gH2AX','pAKT','pERK'}, + 'Lamins':{'LamB1','LamAC', 'LamB2'}} + """ + #os.chdir('./AFSubtracted') + + ls_slide = list(set(df_img.scene)) + #now make overlays + for s_slide, xy_cropcoor in d_crop.items(): + print(f'Processing {s_slide}') + df_slide = df_img[df_img.scene==s_slide] + s_image_round = df_slide[df_slide.marker==s_dapi.split('_')[0]].index[0] + if len(df_slide[df_slide.marker==s_dapi.split('_')[0]].index) == 0: + print('Error: dapi not found') + elif len(df_slide[df_slide.marker==s_dapi.split('_')[0]].index) > 1: + print('Error: too many dapi images found') + else: + print(s_image_round) + #exclude any missing biomarkers + es_all = set(df_slide.marker) + if len(list(set(df_img.imagetype)))==1: + s_imagetype = list(set(df_img.imagetype))[0] + print(s_imagetype) + else: + print('Error: more than one image type)') + for s_type, es_combos in d_combos.items(): + d_overlay = {} + es_combos_shared = es_combos.intersection(es_all) + for idx, s_combo in enumerate(sorted(es_combos_shared)): + s_filename = (df_slide[df_slide.marker==s_combo]).index[0] + if len((df_slide[df_slide.marker==s_combo]).index) == 0: + print('Error: marker not found') + elif len((df_slide[df_slide.marker==s_combo]).index) > 1: + print('Error: too many marker images found') + else: + print(s_filename) + d_overlay.update({s_combo:s_filename}) + d_overlay.update({'1AAADAPI':s_image_round}) + a_size = skimage.io.imread(s_image_round) + #crop + a_crop = a_size[(xy_cropcoor[1]):(xy_cropcoor[1]+tu_dim[1]),(xy_cropcoor[0]):(xy_cropcoor[0]+tu_dim[0])] + a_overlay = np.zeros((len(d_overlay),a_crop.shape[0],a_crop.shape[1]),dtype=np.uint8) + s_biomarker_all = '' + i = -1 + for s_color in sorted(d_overlay.keys()): + i = i + 1 + s_overlay= d_overlay[s_color] + s_biomarker = s_color.split('1AAA')[0] + '.' + s_biomarker_all = s_biomarker_all + s_biomarker + a_size = skimage.io.imread(s_overlay) + #crop + a_channel = a_size[(xy_cropcoor[1]):(xy_cropcoor[1]+tu_dim[1]),(xy_cropcoor[0]):(xy_cropcoor[0]+tu_dim[0])] + if s_imagetype=='ORG': + a_channel = (a_channel/256).astype(np.uint8) + print('covert to 8 bit') + a_overlay[i,:,:] = a_channel + s_biomarker_all = s_biomarker_all[1:-1] + #this works. Open in image j. use Image/Color/Make Composite. Then use + #Image/Color/Channels Tool to turn on and off channels + #use Image/Adjust/Brightness/Contrast to adjust + with skimage.external.tifffile.TiffWriter(f'./{s_type}_{s_dapi.split("_")[0]}.{s_biomarker_all}_{s_slide}_x{xy_cropcoor[0]}y{xy_cropcoor[1]}_overlay.tiff', imagej=True) as tif: + for i in range(a_overlay.shape[0]): + tif.save(a_overlay[i]) + print(f'saved {s_type}') + +def make_thresh_df(df_out,ls_drop=None): + """ + makes a thresholding csv matching the output dataframe (df_out)'s scenes and biomarkers + """ + ls_scene = list(set(df_out.scene)) + ls_scene.append('global_manual') + ls_scene.sort() + ls_biomarker = df_out.columns.tolist() + ls_biomarker.remove('scene') + if ls_drop != None: + for s_drop in ls_drop: + ls_biomarker.remove(s_drop) + ls_manual = [] + for s_biomarker in ls_biomarker: + s_marker = s_biomarker.split('_')[0] + '_manual' + ls_manual.append(s_marker) + ls_manual.sort() + df_thresh = pd.DataFrame(index=ls_scene,columns=ls_manual) + #df_thresh_t = df_thresh.transpose() + return(df_thresh) + +def check_seg(s_sample= 'sampleID',ls_find=['Cell Segmentation Full Color'], i_rows=2, t_figsize=(20,10)): + """ + This script makes overviews of all the specified segmentation images of guillaumes ouput images + in a big folder (slides prepared for segmentation for example) + Input: ls_find = list of images to view + i_rows = number or rows in figure + t_figsize = (x, y) in inches size of figure + b_mkdir = boolean whether to make a new Check_Registration folder (deprecated) + Output: dictionary with {slide_color:number of rounds found} + images of all rounds of a certain slide_color + """ + d_result = {} + #if b_mkdir: + # os.mkdir(f'./Check_Registration') + for s_find in ls_find: + #find all dapi slides + ls_dapis = [] + for s_dir in os.listdir(): + if s_dir.find(s_find) > -1: + ls_dapis = ls_dapis + [s_dir] + ls_dapis.sort() + + #find all unique scenes + ls_scene_long = [] + for s_dapi in ls_dapis: + ls_scene_long = ls_scene_long + [(s_dapi.split('-')[0])] + ls_scene = list(set(ls_scene_long)) + ls_scene.sort() + fig,ax = plt.subplots(i_rows,(len(ls_scene)+(i_rows-1))//i_rows, figsize = t_figsize, squeeze=False) + ax = ax.ravel() + for idx, s_scene in enumerate(ls_scene): + print(f'Processing {s_scene}') + im_low = skimage.io.imread(ls_dapis[idx])#,plugin='simpleitk' + im = skimage.exposure.rescale_intensity(im_low,in_range=(np.quantile(im_low,0.02),np.quantile(im_low,0.98)+np.quantile(im_low,0.98)/2)) + im = skimage.transform.rescale(im, 0.25, anti_aliasing=False) + ax[idx].imshow(im) #, cmap='gray' + ax[idx].set_title(s_scene,{'fontsize':12}) + plt.tight_layout() + #fig.savefig(f'../Check_Registration/{s_sample}_{s_find}.png') + d_result.update({f'{s_sample}_{s_find}.png':fig}) + return(d_result) diff --git a/mplex_image/register.py b/mplex_image/register.py new file mode 100755 index 0000000..b963866 --- /dev/null +++ b/mplex_image/register.py @@ -0,0 +1,105 @@ +import numpy as np +from PIL import Image +from matplotlib import pyplot as plt +from skimage import transform, util +from skimage import data, img_as_float +from skimage.util import img_as_ubyte +import cv2 +import sys + +# code from adapted chandler gatenbee and brian white +# https://github.com/IAWG-CSBC-PSON/registration-challenge + +def match_keypoints(moving, target, feature_detector): + ''' + :param moving: image that is to be warped to align with target image + :param target: image to which the moving image will be aligned + :param feature_detector: a feature detector from opencv + :return: + ''' + + kp1, desc1 = feature_detector.detectAndCompute(moving, None) + kp2, desc2 = feature_detector.detectAndCompute(target, None) + + matcher = cv2.BFMatcher(normType=cv2.NORM_L2, crossCheck=True) + matches = matcher.match(desc1, desc2) + + src_match_idx = [m.queryIdx for m in matches] + dst_match_idx = [m.trainIdx for m in matches] + + src_points = np.float32([kp1[i].pt for i in src_match_idx]) + dst_points = np.float32([kp2[i].pt for i in dst_match_idx]) + + H, mask = cv2.findHomography(src_points, dst_points, cv2.RANSAC, ransacReprojThreshold=10) + + good = [matches[i] for i in np.arange(0, len(mask)) if mask[i] == [1]] + + filtered_src_match_idx = [m.queryIdx for m in good] + filtered_dst_match_idx = [m.trainIdx for m in good] + + filtered_src_points = np.float32([kp1[i].pt for i in filtered_src_match_idx]) + filtered_dst_points = np.float32([kp2[i].pt for i in filtered_dst_match_idx]) + + return filtered_src_points, filtered_dst_points + +def apply_transform(moving, target, moving_pts, target_pts, transformer, output_shape_rc=None): + ''' + :param transformer: transformer object from skimage. See https://scikit-image.org/docs/dev/api/skimage.transform.html for different transformations + :param output_shape_rc: shape of warped image (row, col). If None, uses shape of traget image + return + ''' + if output_shape_rc is None: + output_shape_rc = target.shape[:2] + + if str(transformer.__class__) == "": + transformer.estimate(target_pts, moving_pts) + warped_img = transform.warp(moving, transformer, output_shape=output_shape_rc) + + ### Restimate to warp points + transformer.estimate(moving_pts, target_pts) + warped_pts = transformer(moving_pts) + else: + transformer.estimate(moving_pts, target_pts) + warped_img = transform.warp(moving, transformer.inverse, output_shape=output_shape_rc) + warped_pts = transformer(moving_pts) + + return warped_img, warped_pts + +def keypoint_distance(moving_pts, target_pts, img_h, img_w): + dst = np.sqrt(np.sum((moving_pts - target_pts)**2, axis=1)) / np.sqrt(img_h**2 + img_w**2) + return np.mean(dst) + + + + +def register(target_file,moving_file, b_plot=False): + s_round = moving_file.split('_')[0] + s_sample = moving_file.split('_')[2] + print(s_round) + target = img_as_ubyte(img_as_float(Image.open(target_file))) + moving = img_as_ubyte(img_as_float(Image.open(moving_file))) + + fd = cv2.AKAZE_create() + #fd = cv2.KAZE_create(extended=True) + moving_pts, target_pts = match_keypoints(moving, target, feature_detector=fd) + + transformer = transform.SimilarityTransform() + warped_img, warped_pts = apply_transform(moving, target, moving_pts, target_pts, transformer=transformer) + + warped_img = img_as_ubyte(warped_img) + + print("Unaligned offset:", keypoint_distance(moving_pts, target_pts, moving.shape[0], moving.shape[1])) + print("Aligned offset:", keypoint_distance(warped_pts, target_pts, moving.shape[0], moving.shape[1])) + if b_plot: + fig, ax = plt.subplots(2,2, figsize=(10,10)) + ax[0][0].imshow(target) + ax[0][0].imshow(moving, alpha=0.5) + ax[1][0].scatter(target_pts[:,0], -target_pts[:,1]) + ax[1][0].scatter(moving_pts[:,0], -moving_pts[:,1]) + + ax[0][1].imshow(target) + ax[0][1].imshow(warped_img, alpha=0.5) + ax[1][1].scatter(target_pts[:,0], -target_pts[:,1]) + ax[1][1].scatter(warped_pts[:,0], -warped_pts[:,1]) + plt.savefig(f"../../QC/RegistrationPlots/{s_sample}_{s_round}_rigid_align.png", format="PNG") + return(moving_pts, target_pts, transformer) diff --git a/mplex_image/segment.py b/mplex_image/segment.py new file mode 100755 index 0000000..972742a --- /dev/null +++ b/mplex_image/segment.py @@ -0,0 +1,717 @@ +#### +# title: segment.py +# +# language: Python3.7 +# date: 2020-06-00 +# license: GPL>=v3 +# author: Jenny +# +# description: +# python3 script for cell segmentation +#### +import time +import cellpose +from cellpose import models +from PIL import Image +Image.MAX_IMAGE_PIXELS = 1000000000 + +import os +import skimage +import pandas as pd +import numpy as np +import sys +import scipy +from scipy import stats +from scipy import ndimage as ndi +from skimage import io, filters +from skimage import measure, segmentation, morphology +from numba import jit, types +from numba.extending import overload +from numba.experimental import jitclass +import numba +import mxnet as mx +import stat +from mxnet import nd +from mplex_image import preprocess + +#set src path (CHANGE ME) +s_src_path = '/home/groups/graylab_share/OMERO.rdsStore/engje/Data/cmIF' + +#functions + +def gpu_device(): + try: + _ = mx.nd.array([1, 2, 3], ctx=mx.gpu()) + mx_gpu = mx.gpu() + except mx.MXNetError: + return None + return mx_gpu + +def cellpose_nuc(key,dapi,diameter=30): + ''' + smallest nuclei are about 9 pixels, lymphocyte is 15 pixels, tumor is 25 pixels + using 20 can capture large tumor cells, without sacrificing smaller cells, + ''' + try: + nd_array = mx.nd.array([1, 2, 3], ctx=mx.gpu()) + print(nd_array) + mx_gpu = mx.gpu() + except mx.MXNetError: + print('Mxnet error') + mx_gpu = None + model = models.Cellpose(model_type='nuclei',device=mx_gpu) + newkey = f"{key.split(' - Z')[0]} nuclei{diameter}" + print(f"modelling {newkey}") + channels = [0,0] + print(f'Minimum nuclei size = {int(np.pi*(diameter/10)**2)}') + masks, flows, styles, diams = model.eval(dapi, diameter=diameter, channels=channels,flow_threshold=0,min_size= int(np.pi*(diameter/10)**2)) + return({newkey:masks}) + +def cellpose_cell(key,zdh,diameter=25): + ''' + big tumor cell is 30 pixels, lymphocyte about 18 pixels, small fibroblast 12 pixels + ''' + try: + _ = mx.nd.array([1, 2, 3], ctx=mx.gpu()) + mx_gpu = mx.gpu() + except mx.MXNetError: + mx_gpu = None + model = models.Cellpose(model_type='cyto',device=mx_gpu) + newkey = f"{key.split(' - Z')[0]} cell{diameter}" + print(f"modelling {newkey}") + channels = [2,3] + print(f'Minimum cell size = {int(np.pi*(diameter/5)**2)}') + masks, flows, styles, diams = model.eval(zdh, diameter=diameter, channels=channels,flow_threshold=0.6,cellprob_threshold=0.0, min_size= int(np.pi*(diameter/5)**2)) + return({newkey:masks}) + +def parse_org(s_end = "ORG.tif",s_start='R'): + """ + This function will parse images following koei's naming convention + Example: Registered-R1_PCNA.CD8.PD1.CK19_Her2B-K157-Scene-002_c1_ORG.tif + The output is a dataframe with image filename in index + And rounds, color, imagetype, scene (/tissue), and marker in the columns + """ + s_path = os.getcwd() + ls_file = [] + for file in os.listdir(): + if file.endswith(s_end): + if file.find(s_start)==0: + ls_file = ls_file + [file] + df_img = pd.DataFrame(index=ls_file) + df_img['rounds'] = [item.split('_')[0].split('Registered-')[1] for item in df_img.index] + df_img['color'] = [item.split('_')[-2] for item in df_img.index] + df_img['slide'] = [item.split('_')[2] for item in df_img.index] + df_img['marker_string'] = [item.split('_')[1] for item in df_img.index] + try: + df_img['scene'] = [item.split('-Scene-')[1] for item in df_img.slide] + except: + df_img['scene'] = '001' + df_img['path'] = [f"{s_path}/{item}" for item in df_img.index] + #parse file name for biomarker + for s_index in df_img.index: + #print(s_index) + s_color = df_img.loc[s_index,'color'] + if s_color == 'c1': + s_marker = 'DAPI' + elif s_color == 'c2': + s_marker = s_index.split('_')[1].split('.')[0] + elif s_color == 'c3': + s_marker = s_index.split('_')[1].split('.')[1] + elif s_color == 'c4': + s_marker = s_index.split('_')[1].split('.')[2] + elif s_color == 'c5': + s_marker = s_index.split('_')[1].split('.')[3] + #these are only included in sardana shading corrected images + elif s_color == 'c6': + s_marker = s_index.split('_')[1].split('.')[2] + elif s_color == 'c7': + s_marker = s_index.split('_')[1].split('.')[3] + else: print('Error') + df_img.loc[s_index,'marker'] = s_marker + return(df_img) + +def cmif_mkdir(ls_dir): + ''' + check if directories existe. if not, make them + ''' + for s_dir in ls_dir: + if not os.path.exists(s_dir): + os.makedirs(s_dir) + +def load_single(s_find, s_scene): + ''' + load a single image containing the find strin, scale, return {filename:scaled image} + ''' + d_img = {} + for s_file in os.listdir(): + if s_file.find(s_find)>-1: + a_img = io.imread(s_file) + a_scale = skimage.exposure.rescale_intensity(a_img,in_range=(np.quantile(a_img,0.03),1.5*np.quantile(a_img,0.9999))) + #d_img.update({f"{os.path.splitext(s_file)[0]}":a_scale}) + d_img.update({f"{s_scene}":a_scale}) + print(f'Number of images = {len(d_img)}') + return(d_img) + +def load_stack(df_img,s_find,s_scene,ls_markers,ls_rare): + ''' + load an image stack in df_img, (df_img must have "path") + scale, get mip, return {filename:mip} + ''' + d_img = {} + for s_file in os.listdir(): + if s_file.find(s_find)>-1: + a_img = io.imread(s_file) + dapi = skimage.exposure.rescale_intensity(a_img,in_range=(np.quantile(a_img,0.03),1.5*np.quantile(a_img,0.9999))) + + imgs = [] + #images + df_common = df_img[df_img.marker.isin(ls_markers) & ~df_img.marker.isin(ls_rare)] + df_rare = df_img[df_img.marker.isin(ls_markers) & df_img.marker.isin(ls_rare)] + for s_path in df_common.path: + #print(s_path) + img = io.imread(s_path) + img_scale = skimage.exposure.rescale_intensity(img,in_range=(np.quantile(img,0.03),1.5*np.quantile(img,0.9999))) + imgs.append(img_scale) + for s_path in df_rare.path: + img = io.imread(s_path) + img_scale = skimage.exposure.rescale_intensity(img,in_range=(np.quantile(img,0.03),1.5*np.quantile(img,0.99999))) + imgs.append(img_scale) + mip = np.stack(imgs).max(axis=0) + zdh = np.dstack((np.zeros(mip.shape),mip,dapi)).astype('uint16') + #name + #s_index = df_common.index[0] + #s_common_marker = df_common.loc[s_index,'marker_string'] + #s_name = os.path.splitext(df_common.index[0])[0] + #s_name = s_name.replace(s_common_marker,".".join(ls_markers)) + # name + s_name = f'{s_scene}_{".".join(ls_markers)}' + d_img.update({s_name:zdh}) + print(f'Number of projection images = ({len(d_img)}') + return(d_img) + +def load_img(subdir,s_find,s_sample,s_scene,ls_seg_markers,ls_rare): + ''' + load dapi round and cell segmentation images + ''' + #image dataframe + os.chdir(subdir) + df_seg = pd.DataFrame() + for s_dir in os.listdir(): + if s_dir.find(s_sample)>-1: + os.chdir(s_dir) + df_img = parse_org() + df_markers = df_img[df_img.marker.isin(ls_seg_markers)] + df_markers['path'] = [f'{subdir}/{s_dir}/{item}' for item in df_markers.index] + if df_img.index.str.contains(s_find).sum()==1: + s_file = s_dir + dapi = io.imread(df_img[df_img.index.str.contains(s_find)].index[0]) + os.chdir('..') + df_seg = df_seg.append(df_markers) + + #load z_projection DAPIs + os.chdir(subdir) + d_dapi = {} + d_cyto = {} + + dapi_scale = skimage.exposure.rescale_intensity(dapi,in_range=(np.quantile(dapi,0.03),1.5*np.quantile(dapi,0.9999))) + d_dapi.update({f"{s_sample}-{s_scene}":dapi_scale}) + imgs = [] + #images + df_common = df_seg[(df_seg.scene==s_scene) & (~df_seg.marker.isin(ls_rare))] + df_rare = df_seg[(df_seg.scene==s_scene) & (df_seg.marker.isin(ls_rare))] + for s_path in df_common.path: + print(s_path) + img = io.imread(s_path) + img_scale = skimage.exposure.rescale_intensity(img,in_range=(np.quantile(img,0.03),1.5*np.quantile(img,0.9999))) + imgs.append(img_scale) + for s_path in df_rare.path: + img = io.imread(s_path) + img_scale = skimage.exposure.rescale_intensity(img,in_range=(np.quantile(img,0.03),1.5*np.quantile(img,0.99999))) + imgs.append(img_scale) + mip = np.stack(imgs).max(axis=0) + zdh = np.dstack((np.zeros(mip.shape),mip,dapi)).astype('uint16') + d_cyto.update({f"{s_sample}-{s_scene}":zdh}) + print(f'Number of images = {len(d_dapi)} dapi projections ({len(d_cyto)} cytoplasm projections) ') + + return(d_dapi,d_cyto) + +def cellpose_segment_job(s_sample='SampleName',s_slide_scene="SceneName",s_find="FindDAPIString",segdir='PathtoSegmentation',imgdir='PathtoImages',nuc_diam='30',cell_diam='30',s_type='cell_or_nuclei',s_seg_markers="['Ecad']",s_rare="[]",s_match='both',s_data='cmIF',s_job='cpu'): + """ + makes specific changes to template pyscripts files in Jenny's directories to result in .py file + Input: + """ + #find template, open ,edit + os.chdir(f'{s_src_path}/src') + if s_data == 'cmIF': + with open('cellpose_template.py') as f: + s_file = f.read() + elif s_data == 'codex': + with open('cellpose_template_codex.py') as f: + s_file = f.read() + s_file = s_file.replace('SampleName',s_sample) + s_file = s_file.replace('SceneName',s_slide_scene) + s_file = s_file.replace('FindDAPIString',s_find) + s_file = s_file.replace('nuc_diam=int',f'nuc_diam={str(nuc_diam)}') + s_file = s_file.replace('cell_diam=int',f'cell_diam={str(cell_diam)}') + s_file = s_file.replace('cell_or_nuclei',s_type) + s_file = s_file.replace("['Ecad']",s_seg_markers) + s_file = s_file.replace("ls_rare = []",f"ls_rare = {s_rare}") + s_file = s_file.replace('PathtoSegmentation',segdir) + s_file = s_file.replace('PathtoImages',imgdir) + if s_match == 'match': + s_file = s_file.replace('#MATCHONLY',"'''") + elif s_match == 'seg': + s_file = s_file.replace('#SEGONLY',"'''") + if s_job == 'long': + with open('cellpose_template_long.sh') as f: + s_shell = f.read() + elif s_job == 'gpu': + with open('cellpose_template_gpu.sh') as f: + s_shell = f.read() + s_file = s_file.replace('#gpu#','') + s_file = s_file.replace('#SEGONLY',"'''") + else: + with open('cellpose_template.sh') as f: + s_shell = f.read() + s_shell = s_shell.replace("PythonScripName",f'cellpose_{s_type}_{s_slide_scene}.py') + + #save edited .py file + if s_sample.find("-Scene") > -1: + s_sample = s_sample.split("-Scene")[0] + print(s_sample) + os.chdir(f'{segdir}') + with open(f'cellpose_{s_type}_{s_slide_scene}.py', 'w') as f: + f.write(s_file) + + with open(f'cellpose_{s_type}_{s_slide_scene}.sh', 'w') as f: + f.write(s_shell) + st = os.stat(f'cellpose_{s_type}_{s_slide_scene}.sh') + os.chmod(f'cellpose_{s_type}_{s_slide_scene}.sh', st.st_mode | stat.S_IEXEC) + +def segment_spawner(s_sample,segdir,regdir,nuc_diam=30,cell_diam=30,s_type='nuclei',s_seg_markers="['Ecad']",s_job='short',s_match='both'): + ''' + spawns cellpose segmentation jobs by modifying a python and bash script, saving them and calling with os.system + s_job='gpu' or 'long' (default = 'short') + s_match= 'seg' or 'match' (default = 'both') + ''' + preprocess.cmif_mkdir([f'{segdir}/{s_sample}Cellpose_Segmentation']) + os.chdir(f'{regdir}') + for s_file in os.listdir(): + if s_file.find(s_sample) > -1: + os.chdir(f'{regdir}/{s_file}') + print(f'Processing {s_file}') + df_img = parse_org() + for s_scene in sorted(set(df_img.scene)): + s_slide_scene= f'{s_sample}-Scene-{s_scene}' + s_find = df_img[(df_img.rounds=='R1') & (df_img.color=='c1') & (df_img.scene==s_scene)].index[0] + if os.path.exists(f'{regdir}/{s_slide_scene}'): + cellpose_segment_job(s_file,s_slide_scene,s_find,f'{segdir}/{s_sample}Cellpose_Segmentation',f'{regdir}/{s_slide_scene}',nuc_diam,cell_diam,s_type,s_seg_markers,s_job=s_job, s_match=s_match) + elif os.path.exists(f'{regdir}/{s_sample}'): + cellpose_segment_job(s_file,s_slide_scene,s_find,f'{segdir}/{s_sample}Cellpose_Segmentation',f'{regdir}/{s_sample}',nuc_diam,cell_diam,s_type,s_seg_markers,s_job=s_job, s_match=s_match) + os.chdir(f'{segdir}/{s_sample}Cellpose_Segmentation') + os.system(f'sbatch cellpose_{s_type}_{s_slide_scene}.sh') + time.sleep(4) + print('Next') + +def save_seg(processed_list,segdir,s_type='nuclei'): + ''' + save the segmentation basins + ''' + + for item in processed_list: + for newkey,mask in item.items(): + print(f"saving {newkey.split(' - ')[0]} {s_type} Basins") + if s_type=='nuclei': + io.imsave(f"{segdir}/{newkey} - Nuclei Segmentation Basins.tif", mask) #Scene 002 - Nuclei Segmentation Basins.tif + elif s_type=='cell': + io.imsave(f"{segdir}/{newkey} - Cell Segmentation Basins.tif", mask) #Scene 002 - Nuclei Segmentation Basins.tif + +def save_img(d_img, segdir,s_type='nuclei',ls_seg_markers=[]): + ''' + save the segmentation basins + ''' + #save dapi or save the cyto projection + if s_type=='nuclei': + for key,dapi in d_img.items(): + print('saving DAPI') + print(key) + io.imsave(f"{segdir}/{key} - DAPI.png",dapi) + elif s_type=='cell': + for key,zdh in d_img.items(): + print('saving Cyto Projection') + io.imsave(f"{segdir}/{key.split(' - ')[0]} - {'.'.join(ls_seg_markers)}_CytoProj.png",(zdh/255).astype('uint8')) + + else: + print('choose nuceli or cell') + +# numba functions +kv_ty = (types.int64, types.int64) + +@jitclass([('d', types.DictType(*kv_ty)), + ('l', types.ListType(types.float64))]) +class ContainerHolder(object): + def __init__(self): + # initialize the containers + self.d = numba.typed.Dict.empty(*kv_ty) + self.l = numba.typed.List.empty_list(types.float64) + +@overload(np.array) +def np_array_ol(x): + if isinstance(x, types.Array): + def impl(x): + return np.copy(x) + return impl + +@numba.njit +def test(a): + b = np.array(a) + +# numba function + ''' + use numba to quickly iterate over each label and replace pixels with new pixel values + Input: + container = numba container class, with key-value pairs of old-new cell IDs + labels: numpy array with labels to rename + #cell_labels = np.where(np.array(cell_labels,dtype=np.int64)==key, value, np.array(labels,dtype=np.int64)) + ''' + +@jit(nopython=True) +def relabel_numba(container,cell_labels): + ''' + faster; replace pixels accorind to dictionsry (i.e. numba container) + key is original cell label, value is replaced label + ''' + cell_labels = np.array(cell_labels) + for key, value in container.d.items(): + cell_labels = np.where(cell_labels==key, value, cell_labels) + print('done matching') + return(cell_labels) + +def relabel_numpy(d_replace,cell_labels): + ''' + slow replace pixels accorind to dictionary + key is original cell label, value is replaced label + ''' + #key is original cell albel, value is replaced label + for key, value in d_replace.items(): + cell_labels = np.where(cell_labels==key, value, cell_labels) + print('done matching') + return(cell_labels) + +def relabel_gpu(d_replace,cell_labels): + ''' + not implemented yet + key is original cell label, value is replaced label + ''' + #key is original cell albel, value is replaced label + for key, value in d_replace.items(): + cell_labels = np.where(cell_labels==key, value, cell_labels) + print('done mathcing') + return(cell_labels) + +def nuc_to_cell_new(labels,cell_labels): + ''' + problem - still not giving same result as original function + associate the largest nucleaus contained in each cell segmentation + Input: + labels: nuclear labels + cell_labels: cell labels that need to be matched + Ouput: + container: numba container of key-value pairs of old-new cell IDs + ''' + start = time.time() + #dominant nuclei + props = measure.regionprops_table(cell_labels,labels, properties=(['intensity_image','image','label'])) + df_prop = pd.DataFrame(props) + d_replace = {} + for idx in df_prop.index[::-1]: + label_id = df_prop.loc[idx,'label'] + intensity_image = df_prop.loc[idx,'intensity_image'] + image = df_prop.loc[idx,'image'] + nuc_labels = intensity_image[image & intensity_image!=0] + if len(nuc_labels) == 0: + d_replace.update({label_id:0}) + elif len(np.unique(nuc_labels)) == 1: + d_replace.update({label_id:nuc_labels[0]}) + else: + new_id = scipy.stats.mode(nuc_labels)[0][0] + d_replace.update({label_id:new_id}) + + #convert to numba container + container = ContainerHolder() + for key, value in d_replace.items(): + container.d[key] = value + end = time.time() + print(end - start) + return(container,d_replace, df_prop) + +def nuc_to_cell(labels,cell_labels): + ''' + associate the largest nucleaus contained in each cell segmentation + Input: + labels: nuclear labels + cell_labels: cell labels that need to be matched + Ouput: + container: numba container of key-value pairs of old-new cell IDs + ''' + start = time.time() + #dominant nuclei + d_replace = {} + for idx in np.unique(cell_labels)[::-1]: + if idx == 0: + continue + #iterate over each cell label, find all non-zero values contained within that mask + cell_array = labels[cell_labels == idx] + cell_array =cell_array[cell_array !=0] + #for multiple nuclei, choose largest (most common pixels, i.e. mode) + if len(np.unique(cell_array)) > 1: + new_id = scipy.stats.mode(cell_array, axis=0)[0][0] + d_replace.update({idx:new_id}) + elif len(np.unique(cell_array)) == 1: + d_replace.update({idx:cell_array[0]}) + else: + d_replace.update({idx:0}) + #fix matching bug + d_replace = {item[0]:item[1] for item in sorted(d_replace.items(), key=lambda x: x[1], reverse=True)} + #convert to numba container + container = ContainerHolder() + for key, value in d_replace.items(): + container.d[key] = value + end = time.time() + print(end - start) + return(container,d_replace) + +########## OLD ############## + +def zero_background(cells_relabel): + ''' + in a labelled cell image, set the background to zero + ''' + mode = stats.mode(cells_relabel,axis=0)[0][0][0] + black = cells_relabel.copy() + black[black==mode] = 0 + return(black) + +def nuc_to_cell_watershed(labels,cell_labels,i_small=200): + ''' + associate the largest nucleus contained in each cell segmentation + Input: + labels: nuclear labels + cell_labels: cell labels that need to be matched + Ouput: + new_cell_labels: shrunk so not touching and cleaned of small objects < i_small + container: numba container of key-value pairs of old-new cell IDs + d_replace: python dictionary of key-value pairs + ''' + #cells + cell_boundaries = segmentation.find_boundaries(cell_labels,mode='outer') + shrunk_cells = cell_labels.copy() + shrunk_cells[cell_boundaries] = 0 + foreground = shrunk_cells != 0 + foreground_cleaned = morphology.remove_small_objects(foreground, i_small) + background = ~foreground_cleaned + shrunk_cells[background] = 0 + #problem when we filter + #new_cell_labels = measure.label(foreground_cleaned, background=0) + + #nuclei + cut_labels = labels.copy() + background = ~foreground_cleaned + cut_labels[background] = 0 + labels_in = morphology.remove_small_objects(cut_labels, i_small) + cleaned_nuclei = labels_in + distance = ndi.distance_transform_edt(foreground_cleaned) + labels_out = segmentation.watershed(-distance, labels_in, mask=foreground_cleaned) + + #dominant nuclei + props = measure.regionprops_table(shrunk_cells,labels_out, properties=('min_intensity','max_intensity','mean_intensity')) + df_prop = pd.DataFrame(props) + d_replace = {} + for idx in df_prop.index[::-1]: + #iterate over each cell label, find all non-zero values of watershed expansioncontained within that mask + cell_array = labels_out[shrunk_cells == idx] + if len(np.unique(cell_array)) > 1: + new_id = scipy.stats.mode(cell_array, axis=0)[0][0] + d_replace.update({idx:new_id}) + elif len(np.unique(cell_array)) == 1: + d_replace.update({idx:cell_array[0]}) + else: + d_replace.update({idx:0}) + #convert to numba container + container = ContainerHolder() + for key, value in d_replace.items(): + container.d[key] = value + + return(container) + +def save_seg_z(processed_list,segdir,s_type='nuclei'): + ''' + save the segmentation basins + ''' + + for item in processed_list: + for newkey,mask in item.items(): + print(f"saving {newkey.split(' - Z')[0]} {s_type} Basins") + if s_type=='nuclei': + io.imsave(f"{segdir}/{newkey} - Nuclei Segmentation Basins.tif", mask) #Scene 002 - Nuclei Segmentation Basins.tif + elif s_type=='cell': + io.imsave(f"{segdir}/{newkey} - Cell Segmentation Basins.tif", mask) #Scene 002 - Nuclei Segmentation Basins.tif + +def cellpose_segment_parallel(d_img,s_type='nuclei'): + ''' + Dont use/ segment nuclei or cell + ''' + if s_type=='nuclei': + print('segmenting nuclei') + if __name__ == "__main__": + processed_list = Parallel(n_jobs=len(d_img))(delayed(cellpose_nuc)(key,img,diameter=nuc_diam) for key,img in d_img.items()) + + elif s_type=='cell': + print('segmenting cells') + if __name__ == "__main__": + processed_list = Parallel(n_jobs=len(d_img))(delayed(cellpose_cell)(key,img,diameter=cell_diam) for key,img in d_img.items()) + + else: + print('choose nuceli or cell') + return(processed_list) + +def save_img_z(d_img, segdir,s_type='nuclei',ls_seg_markers=[]): + ''' + save the segmentation basins + ''' + #save dapi or save the cyto projection + if s_type=='nuclei': + for key,dapi in d_img.items(): + print('saving DAPI') + io.imsave(f"{segdir}/{key}",dapi) + elif s_type=='cell': + for key,zdh in d_img.items(): + print('saving Cyto Projection') + io.imsave(f"{segdir}/{key.split(' - Z')[0]} - {'.'.join(ls_seg_markers)}_CytoProj.png",(zdh/255).astype('uint8')) + + else: + print('choose nuceli or cell') + +def cellpose_segment_job_z(s_sample='SampleName',s_scene="SceneName",nuc_diam='20',cell_diam='25',s_type='cell_or_nuclei',s_seg_markers="['Ecad']",s_rare="[]",codedir='PathtoCode'): + """ + makes specific changes to template pyscripts files in Jenny's directories to result in .py file + Input: + + """ + #find template, open ,edit + os.chdir(f'{s_src_path}/src') + with open('cellpose_template_z.py') as f: + s_file = f.read() + s_file = s_file.replace('SampleName',s_sample) + s_file = s_file.replace('SceneName',s_scene) + s_file = s_file.replace('nuc_diam=int',f'nuc_diam={str(nuc_diam)}') + s_file = s_file.replace('cell_diam=int',f'cell_diam={str(cell_diam)}') + s_file = s_file.replace('cell_or_nuclei',s_type) + s_file = s_file.replace("['Ecad']",s_seg_markers) + s_file = s_file.replace("ls_rare = []",f"ls_rare = {s_rare}") + s_file = s_file.replace('PathtoCode',codedir) + + with open('cellpose_template_z.sh') as f: + s_shell = f.read() + s_shell = s_shell.replace("PythonScripName",f'cellpose_{s_type}_{s_scene.replace(" ","-").split("_")[0]}.py') + + #save edited .py file + os.chdir(f'{codedir}/Segmentation/{s_sample}Cellpose_Segmentation') + with open(f'cellpose_{s_type}_{s_scene.replace(" ","-").split("_")[0]}.py', 'w') as f: + f.write(s_file) + + with open(f'cellpose_{s_type}_{s_scene.replace(" ","-").split("_")[0]}.sh', 'w') as f: + f.write(s_shell) + +def load_scene_z(subdir,dapidir,s_sample,s_scene,ls_seg_markers,ls_rare): + ''' + load dapi projection and cell segmentation images + ''' + #image dataframe + os.chdir(subdir) + df_seg = pd.DataFrame() + for s_dir in os.listdir(): + if s_dir.find(s_sample)>-1: + os.chdir(s_dir) + df_img = parse_org() + df_markers = df_img[df_img.marker.isin(ls_seg_markers)] + df_markers['path'] = [f'{subdir}/{s_dir}/{item}' for item in df_markers.index] + os.chdir('..') + df_seg = df_seg.append(df_markers) + + #load z_projection DAPIs + os.chdir(dapidir) + d_dapi = {} + d_cyto = {} + for s_file in sorted(os.listdir()): + #print(s_file) + if s_file.find(f'{s_scene} - ZProjectionDAPI.png')>-1: + dapi = io.imread(s_file) + dapi_scale = skimage.exposure.rescale_intensity(dapi,in_range=(np.quantile(dapi,0.03),1.5*np.quantile(dapi,0.9999))) + d_dapi.update({s_file:dapi_scale}) + s_scene = s_scene.split(' ')[1].split('_')[0] + print(s_scene) + imgs = [] + #images + df_common = df_seg[(df_seg.scene==s_scene) & (~df_markers.marker.isin(ls_rare))] + df_rare = df_seg[(df_seg.scene==s_scene) & (df_markers.marker.isin(ls_rare))] + for s_path in df_common.path: + img = io.imread(s_path) + img_scale = skimage.exposure.rescale_intensity(img,in_range=(np.quantile(img,0.03),1.5*np.quantile(img,0.9999))) + imgs.append(img_scale) + for s_path in df_rare.path: + img = io.imread(s_path) + img_scale = skimage.exposure.rescale_intensity(img,in_range=(np.quantile(img,0.03),1.5*np.quantile(img,0.999999))) + imgs.append(img_scale) + mip = np.stack(imgs).max(axis=0) + zdh = np.dstack((np.zeros(mip.shape),mip,dapi)).astype('uint16') + d_cyto.update({s_file:zdh}) + print(f'Number of images = {len(d_dapi)} dapi projections ({len(d_cyto)} cytoplasm projections) ') + + return(d_dapi,d_cyto) + +#test code +''' +import napari +#os.chdir('./Desktop/BR1506') +labels = io.imread('Scene 059 nuclei20 - Nuclei Segmentation Basins.tif') +cell_labels = io.imread('Scene 059 cell25 - Cell Segmentation Basins.tif') +cyto_img = io.imread('Scene 059 - CytoProj.png') +dapi_img = io.imread('Scene 059 - ZProjectionDAPI.png') +viewer = napari.Viewer() +viewer.add_labels(labels,blending='additive') +viewer.add_labels(cell_labels,blending='additive') +viewer.add_image(cyto_img,blending='additive') +viewer.add_image(dapi_img,blending='additive',colormap='blue') +#cell_boundaries = segmentation.find_boundaries(cell_labels,mode='outer') +#viewer.add_labels(cell_boundaries,blending='additive') +#nuclear_boundaries = segmentation.find_boundaries(labels,mode='outer') +#viewer.add_labels(nuclear_boundaries,blending='additive',num_colors=2) +closing = skimage.morphology.closing(cell_labels) +viewer.add_labels(closing,blending='additive') +container = nuc_to_cell(labels,closing)#cell_labels) + +#matched cell labels +cells_relabel = relabel_numba(container[0],closing) +#remove background +mode = stats.mode(cells_relabel,axis=0)[0][0][0] +black = cells_relabel.copy() +black[black==mode] = 0 +viewer.add_labels(black,blending='additive') +cell_boundaries = segmentation.find_boundaries(cells_relabel,mode='outer') +viewer.add_labels(cell_boundaries,blending='additive') +#ring +overlap = black==labels +viewer.add_labels(overlap, blending='additive') +#cytoplasm +ring_rep = black.copy() +ring_rep[overlap] = 0 +viewer.add_labels(ring_rep, blending='additive') +#membrane +rim_labels = contract_membrane(black) +viewer.add_labels(rim_labels, blending='additive') + +#expanded nucleus +__,__,peri_nuc = expand_nuc(labels,distance=3) +viewer.add_labels(peri_nuc, blending='additive') +''' \ No newline at end of file diff --git a/mplex_image/visualize.py b/mplex_image/visualize.py new file mode 100755 index 0000000..3cbdf35 --- /dev/null +++ b/mplex_image/visualize.py @@ -0,0 +1,387 @@ +#### +# title: analyze.py +# +# language: Python3.6 +# date: 2019-05-00 +# license: GPL>=v3 +# author: Jenny +# +# description: +# python3 library to visualize cyclic data and analysis +#### + +#load libraries +import matplotlib as mpl +import matplotlib.pyplot as plt +import pandas as pd +import numpy as np +import os +import skimage +from skimage import io, segmentation +import tifffile +import copy +import napari +import seaborn as sns +from sklearn.cluster import KMeans +from sklearn.preprocessing import scale +import random +import copy +from scipy.ndimage import distance_transform_edt + +#napari +def load_crops(viewer,s_crop,s_tissue): + ls_color = ['blue','green','yellow','red','cyan','magenta','gray','green','yellow','red','cyan','magenta','gray', + 'green','yellow','red','cyan','magenta','gray','gray','gray','gray','gray','gray','gray','gray'] + print(s_crop) + #viewer = napari.Viewer() + for s_file in os.listdir(): + if s_file.find(s_tissue)>-1: + if s_file.find(s_crop) > -1: + if s_file.find('ome.tif') > -1: + with tifffile.TiffFile(s_file) as tif: + array = tif.asarray() + omexml_string = tif.ome_metadata + for idx in range(array.shape[0]): + img = array[idx] + i_begin = omexml_string.find(f'Channel ID="Channel:0:{idx}" Name="') + i_end = omexml_string[i_begin:].find('" SamplesPerPixel') + s_marker = omexml_string[i_begin + 31:i_begin + i_end] + if s_marker.find('utf-8') == 0: + s_marker = 'DAPI1' + print(s_marker) + viewer.add_image(img,name=s_marker,rgb=False,visible=False,blending='additive',colormap=ls_color[idx],contrast_limits = (np.quantile(img,0),(np.quantile(img,0.9999)+1)*1.5)) + elif s_file.find('SegmentationBasins') > -1: + label_image = io.imread(s_file) + viewer.add_labels(label_image, name='cell_seg',blending='additive',visible=False) + cell_boundaries = segmentation.find_boundaries(label_image,mode='outer') + viewer.add_labels(cell_boundaries,blending='additive',visible=False) + else: + label_image = np.array([]) + print('') + return(label_image) + +def load_marker(viewer,s_crop,s_tissue,ls_marker=[]): + ls_color = ['blue','green','yellow','red','cyan','magenta','gray','green','yellow','red','cyan','magenta', + 'gray','gray','gray','gray','gray','gray','gray','gray'] + print(s_crop) + ls_marker_all = copy.copy(ls_marker) + for s_file in os.listdir(): + if s_file.find(s_tissue)>-1: + if s_file.find(s_crop) > -1: + if s_file.find('ome.tif') > -1: + with tifffile.TiffFile(s_file) as tif: + array = tif.asarray() + omexml_string = tif.ome_metadata + d_result = {} + for idx in range(array.shape[0]): + img = array[idx] + i_begin = omexml_string.find(f'Channel ID="Channel:0:{idx}" Name="') + i_end = omexml_string[i_begin:].find('" SamplesPerPixel') + s_marker_idx = omexml_string[i_begin + 31:i_begin + i_end] + if s_marker_idx.find('utf-8') == 0: + s_marker_idx = 'DAPI1' + d_result.update({s_marker_idx:img}) + for idxs, s_marker in enumerate(ls_marker): + if len(set(d_result.keys()).intersection(set([s_marker])).intersection(set(ls_marker_all))) > 0: + img = d_result[s_marker] + viewer.add_image(img,name=s_marker,rgb=False,visible=True,blending='additive',colormap=ls_color[idxs],contrast_limits = (np.quantile(img,0),(np.quantile(img,0.9999)+1)*1.5)) + ls_marker_all.remove(s_marker) + elif s_file.find('SegmentationBasins') > -1: + label_image = io.imread(s_file) + else: + ome_array = np.array([]) + print('') + return(d_result,label_image) + +def pos_label(viewer,df_pos,label_image,s_cell): + ''' + df_pos = boolean dataframe, s_cell = marker name + ''' + #s_cell = df_pos.columns[df_pos.columns.str.contains(f'{s_cell}_')][0] + #get rid of extra cells (filtered by DAPI, etc) + li_index = [int(item.split('_')[-1].split('cell')[1]) for item in df_pos.index] + label_image_cell = copy.deepcopy(label_image) + label_image_cell[~np.isin(label_image_cell, li_index)] = 0 + li_index_cell = [int(item.split('_')[-1].split('cell')[1]) for item in df_pos[df_pos.loc[:,s_cell]==True].index] + label_image_cell[~np.isin(label_image_cell,li_index_cell )] = 0 + viewer.add_labels(label_image_cell, name=f'{s_cell.split("_")[0]}_seg',blending='additive',visible=False) + return(label_image_cell) + +def expand_labels(label_image, distance=1): + """Expand labels in label image by ``distance`` pixels without overlapping. + Given a label image, ``expand_labels`` grows label regions (connected components) + outwards by up to ``distance`` pixels without overflowing into neighboring regions. + More specifically, each background pixel that is within Euclidean distance + of <= ``distance`` pixels of a connected component is assigned the label of that + connected component. + Where multiple connected components are within ``distance`` pixels of a background + pixel, the label value of the closest connected component will be assigned (see + Notes for the case of multiple labels at equal distance). + + Parameters + ---------- + label_image : ndarray of dtype int + label image + distance : float + Euclidean distance in pixels by which to grow the labels. Default is one. + Returns + ------- + enlarged_labels : ndarray of dtype int + Labeled array, where all connected regions have been enlarged + """ + distances, nearest_label_coords = distance_transform_edt( + label_image == 0, return_indices=True + ) + labels_out = np.zeros_like(label_image) + dilate_mask = distances <= distance + # build the coordinates to find nearest labels, + # in contrast to [1] this implementation supports label arrays + # of any dimension + masked_nearest_label_coords = [ + dimension_indices[dilate_mask] + for dimension_indices in nearest_label_coords + ] + nearest_labels = label_image[tuple(masked_nearest_label_coords)] + labels_out[dilate_mask] = nearest_labels + return labels_out + +def pos_boundary(viewer,df_pos,label_image,s_cell,seed=0.82,s_type='thick'): + ''' + df_pos = boolean dataframe, s_cell = marker name + ''' + #s_cell = df_pos.columns[df_pos.columns.str.contains(f'{s_cell}_')][0] + #get rid of extra cells (filtered by DAPI, etc) + li_index = [int(item.split('_')[-1].split('cell')[1]) for item in df_pos.index] + label_image_cell = copy.deepcopy(label_image) + label_image_cell[~np.isin(label_image_cell, li_index)] = 0 + li_index_cell = [int(item.split('_')[-1].split('cell')[1]) for item in df_pos[df_pos.loc[:,s_cell]==True].index] + label_image_cell[~np.isin(label_image_cell,li_index_cell )] = 0 + cell_boundaries = segmentation.find_boundaries(label_image_cell,mode='thick') + if s_type == 'thick': + cell_boundaries_big = segmentation.find_boundaries(expand_labels(label_image_cell, distance=2),mode='thick') + viewer.add_labels(cell_boundaries + cell_boundaries_big, name=f'{s_cell}_seg',blending='additive',visible=False,seed=seed) + else: + viewer.add_labels(cell_boundaries, name=f'{s_cell}_seg',blending='additive',visible=False,seed=seed) + cell_boundaries_big = [] + return(cell_boundaries, cell_boundaries_big) + +#jupyter notbook +#load manual thresholds +def new_thresh_csv(df_mi,d_combos): + #make thresh csv's + df_man = pd.DataFrame(index= ['global']+ sorted(set(df_mi.slide_scene))) + for s_type, es_marker in d_combos.items(): + for s_marker in sorted(es_marker): + df_man[s_marker] = '' + return(df_man) + +def load_thresh_csv(s_sample): + #load + df_man = pd.read_csv(f'thresh_JE_{s_sample}.csv',header=0,index_col = 0) + #reformat the thresholds data and covert to 16 bit + ls_index = df_man.index.tolist() + ls_index.remove('global') + df_thresh = pd.DataFrame(index = ls_index) + ls_marker = df_man.columns.tolist() + for s_marker in ls_marker: + df_thresh[f'{s_marker}_global'] = df_man[df_man.index=='global'].loc['global',f'{s_marker}']*256 + df_thresh[f'{s_marker}_local'] = df_man[df_man.index!='global'].loc[:,f'{s_marker}']*256 + + df_thresh.replace(to_replace=0, value = 12, inplace=True) + return(df_thresh) + +def threshold_postive(df_thresh,df_mi): + ''' + #make positive dataframe to check threhsolds #start with local, and if its not there, inesrt the global threshold + #note, this will break if there are two biomarker locations # + ''' + ls_scene = sorted(df_thresh.index.tolist()) + ls_sub = df_mi.columns[df_mi.dtypes=='float64'].tolist() + ls_other = [] + df_pos= pd.DataFrame() + d_thresh_record= {} + for s_scene in ls_scene: + ls_index = df_mi[df_mi.slide_scene==s_scene].index + df_scene = pd.DataFrame(index=ls_index) + for s_marker_loc in ls_sub: + s_marker = s_marker_loc.split('_')[0] + # only threshold markers in .csv + if len(set([item.split('_')[0] for item in df_thresh.columns]).intersection({s_marker})) != 0: + #first check if local threshold exists + if df_thresh[df_thresh.index==s_scene].isna().loc[s_scene,f'{s_marker}_local']==False: + #local + i_thresh = df_thresh.loc[s_scene,f'{s_marker}_local'] + df_scene.loc[ls_index,s_marker_loc] = df_mi.loc[ls_index,s_marker_loc] >= i_thresh + #otherwise use global + elif df_thresh[df_thresh.index==s_scene].isna().loc[s_scene,f'{s_marker}_global']==False: + i_thresh = df_thresh.loc[s_scene,f'{s_marker}_global'] + df_scene.loc[ls_index,s_marker_loc] = df_mi.loc[ls_index,s_marker_loc] >= i_thresh + else: + ls_other = ls_other + [s_marker] + i_thresh = np.NaN + d_thresh_record.update({f'{s_scene}_{s_marker}':i_thresh}) + else: + ls_other = ls_other + [s_marker] + df_pos = df_pos.append(df_scene) + print(f'Did not threshold {set(ls_other)}') + return(d_thresh_record,df_pos) + +def plot_positive(s_type,d_combos,df_pos,d_thresh_record,df_xy,b_save=True): + ls_color = sorted(d_combos[s_type]) + ls_bool = [len(set([item.split('_')[0]]).intersection(set(ls_color)))==1 for item in df_pos.columns] + ls_color = df_pos.columns[ls_bool].tolist() + ls_scene = sorted(set(df_xy.slide_scene)) + ls_fig = [] + for s_scene in ls_scene: + #negative cells = all cells even before dapi filtering + df_neg = df_xy[(df_xy.slide_scene==s_scene)] + #plot + fig, ax = plt.subplots(2, ((len(ls_color))+1)//2, figsize=(18,12)) #figsize=(18,12) + ax = ax.ravel() + for ax_num, s_color in enumerate(ls_color): + s_marker = s_color.split('_')[0] + s_min = d_thresh_record[f"{s_scene}_{s_marker}"] + #positive cells = positive cells based on threshold + ls_pos_index = (df_pos[df_pos.loc[:,s_color]]).index + df_color_pos = df_neg[df_neg.index.isin(ls_pos_index)] + if len(df_color_pos)>=1: + #plot negative cells + ax[ax_num].scatter(data=df_neg,x='DAPI_X',y='DAPI_Y',color='silver',s=1) + #plot positive cells + ax[ax_num].scatter(data=df_color_pos, x='DAPI_X',y='DAPI_Y',color='DarkBlue',s=.5) + + ax[ax_num].axis('equal') + ax[ax_num].set_ylim(ax[ax_num].get_ylim()[::-1]) + ax[ax_num].set_title(f'{s_marker} min={int(s_min)} ({len(df_color_pos)} cells)') + else: + ax[ax_num].set_title(f'{s_marker} min={(s_min)} ({(0)} cells') + fig.suptitle(s_scene) + ls_fig.append(fig) + if b_save: + fig.savefig(f'./SpatialPlots/{s_scene}_{s_type}_manual.png') + return(ls_fig) + +#gating analysis +def prop_positive(df_data,s_cell,s_grouper): + #df_data['countme'] = True + df_cell = df_data.loc[:,[s_cell,s_grouper,'countme']].dropna() + df_prop = (df_cell.groupby([s_cell,s_grouper]).countme.count()/df_cell.groupby([s_grouper]).countme.count()).unstack().T + return(df_prop) + +def prop_clustermap(df_prop,df_annot,i_thresh,lut,figsize=(10,5)): + for s_index in df_prop.index: + s_subtype = df_annot.loc[s_index,'ID'] # + df_prop.loc[s_index, 'ID'] = s_subtype + species = df_prop.pop("ID") + row_colors = species.map(lut) + + #clustermap plot wihtout the low values -drop less than i_threh % of total + df_plot = df_prop.fillna(0) + if i_thresh > 0: + df_plot_less = df_plot.loc[:,df_plot.sum()/len(df_plot) > i_thresh] + i_len = len(df_prop) + i_width = len(df_plot_less.columns) + g = sns.clustermap(df_plot_less,figsize=figsize,cmap='viridis',row_colors=row_colors) + return(g,df_plot_less) + +def prop_barplot(df_plot_less,s_cell,colormap="Spectral",figsize=(10,5),b_sort=True): + i_len = len(df_plot_less) + i_width = len(df_plot_less.columns) + fig,ax = plt.subplots(figsize=figsize) + if b_sort: + df_plot_less = df_plot_less.sort_index(ascending=False) + df_plot_less.plot(kind='barh',stacked=True,width=.9, ax=ax,colormap=colormap) + ax.set_title(s_cell) + ax.set_xlabel('Fraction Positive') + ax.legend(bbox_to_anchor=(1.01, 1)) + plt.tight_layout() + return(fig) + +def plot_color_leg(lut,figsize = (2.3,3)): + #colors + series = pd.Series(lut) + df_color = pd.DataFrame(index=range(len(series)),columns=['subtype','color']) + + series.sort_values() + df_color['subtype'] = series.index + df_color['value'] = 1 + df_color['color'] = series.values + + fig,ax = plt.subplots(figsize = figsize,dpi=100) + df_color.plot(kind='barh',x='subtype',y='value',width=1,legend=False,color=df_color.color,ax=ax) + ax.set_xticks([]) + ax.set_ylabel('') + ax.set_title(f'subtype') + plt.tight_layout() + return(fig) + +#cluster analysis + +def cluster_kmeans(df_mi,ls_columns,k,b_sil=False): + ''' + log2 transform, zscore and kmens cluster + ''' + df_cluster_norm = df_mi.loc[:,ls_columns] + df_cluster_norm_one = df_cluster_norm + 1 + df_cluster = np.log2(df_cluster_norm_one) + + #select figure size + i_len = k + i_width = len(df_cluster.columns) + + #scale date + df_scale = scale(df_cluster) + + #kmeans cluster + kmeans = KMeans(n_clusters=k, random_state=0).fit(df_scale) + df_cluster.columns = [item.split('_')[0] for item in df_cluster.columns] + df_cluster[f'K{k}'] = list(kmeans.labels_) + g = sns.clustermap(df_cluster.groupby(f'K{k}').mean(),cmap="RdYlGn_r",z_score=1,figsize=(3+i_width/3,3+i_len/3)) + if b_sil: + score = silhouette_score(X = df_scale, labels=list(kmeans.labels_)) + else: + score = np.nan + return(g,df_cluster,score) + +def plot_clusters(df_cluster,df_xy,s_num='many'): + s_type = df_cluster.columns[df_cluster.dtypes=='int64'][0] + print(s_type) + ls_scene = sorted(set(df_cluster.slide_scene)) + ls_color = sorted(set(df_cluster.loc[:,s_type].dropna())) + d_fig = {} + for s_scene in ls_scene: + #negative cells = all cells even before dapi filtering + df_neg = df_xy[(df_xy.slide_scene==s_scene)] + #plot + if s_num == 'many': + fig, ax = plt.subplots(3, ((len(ls_color))+2)//3, figsize=(18,12),dpi=200) + else: + fig, ax = plt.subplots(2, 1, figsize=(7,4),dpi=200) + ax = ax.ravel() + for ax_num, s_color in enumerate(ls_color): + s_marker = s_color + #positive cells = poitive cells based on threshold + ls_pos_index = (df_cluster[df_cluster.loc[:,s_type]==s_color]).index + df_color_pos = df_neg[df_neg.index.isin(ls_pos_index)] + if len(df_color_pos)>=1: + #plot negative cells + ax[ax_num].scatter(data=df_neg,x='DAPI_X',y='DAPI_Y',color='silver',s=1) + #plot positive cells + ax[ax_num].scatter(data=df_color_pos, x='DAPI_X',y='DAPI_Y',color='DarkBlue',s=.5) + + ax[ax_num].axis('equal') + ax[ax_num].set_ylim(ax[ax_num].get_ylim()[::-1]) + if s_num == 'many': + ax[ax_num].set_xticklabels('') + ax[ax_num].set_yticklabels('') + else: + ax[0].set_xticklabels('') + ax[ax_num].set_title(f'{s_color} ({len(df_color_pos)} cells)') + else: + ax[ax_num].set_xticklabels('') + ax[ax_num].set_yticklabels('') + ax[ax_num].set_title(f'{s_color} ({(0)} cells') + + fig.suptitle(s_scene) + d_fig.update({s_scene:fig}) + return(d_fig)