Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ venv/
ENV/
env.bak/
venv.bak/
deepcell-env/

# Spyder project settings
.spyderproject
Expand Down
5 changes: 5 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
2 changes: 2 additions & 0 deletions backend/deepcell_label/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def create_project_from_dropped_file():
"""
start = timeit.default_timer()
input_file = request.files.get('images')

axes = request.form['axes'] if 'axes' in request.form else None
# axes = request.form['axes'] if 'axes' in request.form else DCL_AXES
with tempfile.NamedTemporaryFile(delete=DELETE_TEMP) as f:
Expand All @@ -138,6 +139,7 @@ def create_project_from_dropped_file():
input_file.filename,
timeit.default_timer() - start,
)
# current_app.logger.info(IMAGE_FILE)
return jsonify(project.project)


Expand Down
26 changes: 26 additions & 0 deletions backend/deepcell_label/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import asyncio
import pickle
import zlib

import websockets
from flask import current_app


async def perform_send(to_send):
uri = 'ws://131.215.2.187:8765'
async with websockets.connect(uri, ping_interval=None) as websocket:
data = {'img': to_send}
print(uri)
pkt = zlib.compress(pickle.dumps(data))
await websocket.send(pkt)
print('sent')
pkt_received = await websocket.recv()
print('received')
mask = pickle.loads(zlib.decompress(pkt_received))
return mask


def send_to_server(to_send):
current_app.logger.info('Sent to server to generate mask for cellSAM')
mask = asyncio.run(perform_send(to_send))
return mask
48 changes: 45 additions & 3 deletions backend/deepcell_label/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@

import numpy as np
import skimage

# from flask import current_app
from skimage import filters
from skimage.exposure import rescale_intensity
from skimage.measure import regionprops
from skimage.morphology import dilation, disk, erosion, flood, square
from skimage.segmentation import morphological_chan_vese, watershed

from deepcell_label.client import send_to_server


class Edit(object):
"""
Expand All @@ -39,7 +43,6 @@ def __init__(self, labels_zip):

self.valid_modes = ['overlap', 'overwrite', 'exclude']
self.raw_required = ['watershed', 'active_contour', 'threshold']

self.load(labels_zip)
self.dispatch_action()
self.write_response_zip()
Expand All @@ -65,7 +68,6 @@ def load(self, labels_zip):
if not zipfile.is_zipfile(labels_zip):
raise ValueError('Attached labels.zip is not a zip file.')
zf = zipfile.ZipFile(labels_zip)

# Load edit args
if 'edit.json' not in zf.namelist():
raise ValueError('Attached labels.zip must contain edit.json.')
Expand All @@ -76,6 +78,9 @@ def load(self, labels_zip):
self.action = edit['action']
self.height = edit['height']
self.width = edit['width']
self.d1, self.d2, self.d3, self.d4 = [
edit[k] for k in ['d1', 'd2', 'd3', 'd4']
]
self.args = edit.get('args', None)
# TODO: specify write mode per cell?
self.write_mode = edit.get('writeMode', 'overlap')
Expand All @@ -102,7 +107,11 @@ def load(self, labels_zip):
if 'raw.dat' in zf.namelist():
with zf.open('raw.dat') as f:
raw = np.frombuffer(f.read(), np.uint8)
self.raw = np.reshape(raw, (self.width, self.height))
# self.rawOriginal = np.reshape(raw, (self.d1, self.d2, self.d3, self.d4))[:, 0, :, :]
self.rawOriginal = np.reshape(raw, (self.d1, self.d2, self.d3, self.d4))
# self.rawOriginal = np.reshape(self.rawOriginal, (self.d3, self.d4, self.d1))
self.raw = self.rawOriginal[0][0]
# raise ValueError
elif self.action in self.raw_required:
raise ValueError(
f'Include raw array in raw.json to use action {self.action}.'
Expand Down Expand Up @@ -418,3 +427,36 @@ def action_dilate(self, cell):
mask = self.get_mask(cell)
dilated = dilation(mask, square(3))
self.add_mask(dilated, cell)

def action_select_channels(self, channels):
self.nuclear_channel = int(channels[0])
self.wholecell_channel = int(channels[1])

def action_segment_all(self, channels):
nuclear_channel = channels[0]
wholecell_channel = channels[1]
to_send = []
if self.d1 == 1:
to_send = self.raw.reshape(self.d3, self.d4, self.d1)
elif self.d1 > 1:
nuclear = self.rawOriginal[nuclear_channel][0]
wholecell = self.rawOriginal[wholecell_channel][0]
to_send = np.stack([nuclear, wholecell], axis=-1)
mask = send_to_server(to_send)
self.labels = mask.astype(np.int32)
if len(self.labels.shape) == 2:
self.labels = np.expand_dims(np.expand_dims(self.labels, 0), 3)
cells = []
for t in range(self.labels.shape[0]):
for c in range(self.labels.shape[-1]):
for value in np.unique(self.labels[t, :, :, c]):
if value != 0:
cells.append(
{
'cell': int(value),
'value': int(value),
't': int(t),
'c': int(c),
}
)
self.cells = cells
38 changes: 25 additions & 13 deletions backend/deepcell_label/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@

import magic
import numpy as np
from flask import current_app
from PIL import Image
from tifffile import TiffFile, TiffWriter

# from deepcell_label.client import send_to_server
from deepcell_label.utils import convert_lineage, reshape


Expand All @@ -42,6 +44,7 @@ def __init__(self, image_file=None, label_file=None, axes=None):
self.embeddings = None

self.image_file = image_file
current_app.logger.info(image_file)
self.label_file = label_file if label_file else image_file
self.axes = axes

Expand Down Expand Up @@ -86,7 +89,11 @@ def write_images(self):
ValueError: no image data has been loaded to write
"""
X = self.X
# send_to_server(X, "X")
if X is not None:
if len(X.shape) == 3:
X = np.expand_dims(X, 0)
self.X = X
# Move channel axis
X = np.moveaxis(X, -1, 1)
images = io.BytesIO()
Expand All @@ -101,12 +108,15 @@ def write_images(self):
)
images.seek(0)
self.zip.writestr('X.ome.tiff', images.read())
# else:
# raise ValueError('No images found in files')

def write_segmentation(self):
"""Writes segmentation to y.ome.tiff in the output zip."""
y = self.y
# send_to_server(y, "y")
if len(y.shape) == 2:
y = np.expand_dims(np.expand_dims(y, 0), 3)
self.y = y
if y.shape[:-1] != self.X.shape[:-1]:
raise ValueError(
'Segmentation shape %s is incompatible with image shape %s'
Expand Down Expand Up @@ -147,18 +157,19 @@ def write_cells(self):
"""Writes cells to cells.json in the output zip."""
if self.cells is None:
cells = []
for t in range(self.y.shape[0]):
for c in range(self.y.shape[-1]):
for value in np.unique(self.y[t, :, :, c]):
if value != 0:
cells.append(
{
'cell': int(value),
'value': int(value),
't': int(t),
'c': int(c),
}
)
# currently disable instant segmentation upon loading
# for t in range(self.y.shape[0]):
# for c in range(self.y.shape[-1]):
# for value in np.unique(self.y[t, :, :, c]):
# if value != 0:
# cells.append(
# {
# 'cell': int(value),
# 'value': int(value),
# 't': int(t),
# 'c': int(c),
# }
# )
self.cells = cells
self.zip.writestr('cells.json', json.dumps(self.cells))

Expand All @@ -182,6 +193,7 @@ def load_images(image_file, axes=None):
X = load_png(image_file)
if X is None:
X = load_trk(image_file, filename='raw.npy')
print(X.shape)
return X


Expand Down
3 changes: 2 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ mysqlclient~=2.1.0
numpy
python-decouple~=3.1
python-dotenv~=0.19.2
python-magic~=0.4.25
# python-magic~=0.4.25
requests~=2.29.0
scikit-image~=0.19.0
sqlalchemy~=1.3.24
tifffile
imagecodecs
websockets
7 changes: 7 additions & 0 deletions cypress.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
};
5 changes: 5 additions & 0 deletions cypress/fixtures/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "[email protected]",
"body": "Fixtures are a great way to mock data for responses to routes"
}
25 changes: 25 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
20 changes: 20 additions & 0 deletions cypress/support/e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands';

// Alternatively you can use CommonJS syntax:
// require('./commands')
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Slider from '@mui/material/Slider';
// import Tooltip from '@mui/material/Tooltip';
import { useSelector } from '@xstate/react';
import React, { useEffect } from 'react';
import { useLabeled, useMousetrapRef } from '../../ProjectContext';
import { useLabeled, useMousetrapRef, useLabelMode } from '../../ProjectContext';

let numMounted = 0;

Expand All @@ -18,6 +18,9 @@ function CellsOpacitySlider() {

const handleDoubleClick = () => labeled.send({ type: 'SET_CELLS_OPACITY', opacity: 0.3 }); // [0.3, 1] for range slider

const labelMode = useLabelMode();
const isCellTypes = useSelector(labelMode, (state) => state.matches('editCellTypes'));

useEffect(() => {
const listener = (e) => {
if (e.key === 'z') {
Expand Down Expand Up @@ -47,7 +50,7 @@ function CellsOpacitySlider() {
return (
<Tooltip title={<kbd>Z</kbd>} placement='right'>
<Slider
value={opacity}
value={isCellTypes ? 0 : opacity}
min={0}
max={1}
// track={false}
Expand All @@ -57,6 +60,7 @@ function CellsOpacitySlider() {
onDoubleClick={handleDoubleClick}
componentsProps={{ input: { ref: inputRef } }}
sx={{ py: 0, '& .MuiSlider-thumb': { height: 15, width: 15 } }}
disabled={isCellTypes}
/>
</Tooltip>
);
Expand Down
20 changes: 13 additions & 7 deletions frontend/src/Project/EditControls/EditControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CellControls from './CellControls';
import CellTypeControls from './CellTypeControls';
import TrackingControls from './DivisionsControls';
import SegmentControls from './SegmentControls';
import SegmentSamControls from './SegmentSamControls';

function TabPanel(props) {
const { children, value, index, ...other } = props;
Expand All @@ -28,14 +29,16 @@ function EditControls() {
const value = useSelector(labelMode, (state) => {
return state.matches('editSegment')
? 0
: state.matches('editCells')
: state.matches('editSegmentSam')
? 1
: state.matches('editDivisions')
: state.matches('editCells')
? 2
: state.matches('editCellTypes')
: state.matches('editDivisions')
? 3
: state.matches('editSpots')
: state.matches('editCellTypes')
? 4
: state.matches('editSpots')
? 5
: false;
});

Expand All @@ -51,15 +54,18 @@ function EditControls() {
<SegmentControls />
</TabPanel>
<TabPanel value={value} index={1}>
<CellControls />
<SegmentSamControls />
</TabPanel>
<TabPanel value={value} index={2}>
<TrackingControls />
<CellControls />
</TabPanel>
<TabPanel value={value} index={3}>
<CellTypeControls />
<TrackingControls />
</TabPanel>
<TabPanel value={value} index={4}>
<CellTypeControls />
</TabPanel>
<TabPanel value={value} index={5}>
<SpotsControls />
</TabPanel>
</Box>
Expand Down
Loading