diff --git a/frontend/javascripts/oxalis/view/action-bar/download_modal_view.tsx b/frontend/javascripts/oxalis/view/action-bar/download_modal_view.tsx index 17e3eda310..d0b9063cda 100644 --- a/frontend/javascripts/oxalis/view/action-bar/download_modal_view.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/download_modal_view.tsx @@ -20,7 +20,7 @@ import { BoundingBoxSelection, getReadableNameOfVolumeLayer, MagSlider, -} from "oxalis/view/right-border-tabs/starting_job_modals"; +} from "oxalis/view/action-bar/starting_job_modals"; import { getUserBoundingBoxesFromState } from "oxalis/model/accessors/tracing_accessor"; import { getVolumeTracingById, diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/starting_job_modals.tsx b/frontend/javascripts/oxalis/view/action-bar/starting_job_modals.tsx similarity index 98% rename from frontend/javascripts/oxalis/view/right-border-tabs/starting_job_modals.tsx rename to frontend/javascripts/oxalis/view/action-bar/starting_job_modals.tsx index d63801339d..e303a3df4c 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/starting_job_modals.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/starting_job_modals.tsx @@ -26,7 +26,7 @@ import { clamp, computeArrayFromBoundingBox, rgbToHex } from "libs/utils"; import { getBaseSegmentationName } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; import { V3 } from "libs/mjs"; import { ResolutionInfo } from "oxalis/model/helpers/resolution_info"; -import { isBoundingBoxExportable } from "../action-bar/download_modal_view"; +import { isBoundingBoxExportable } from "./download_modal_view"; import features from "features"; const { ThinSpace } = Unicode; @@ -538,6 +538,16 @@ export function NucleiSegmentationModal({ handleClose }: Props) { } export function NeuronSegmentationModal({ handleClose }: Props) { const dataset = useSelector((state: OxalisState) => state.dataset); + return ( + + + Automated Analysis with WEBKNOSSOS + + } + > + ); return ( = { + "neuron inferral": "neuron_inferral_example.jpg", + "nuclei inferral": "nuclei_inferral_example.jpg", + "materialize volume annotation": "materialize_volume_annotation_example.jpg", +}; +type Props = { + handleClose: () => void; +}; + +type JobApiCallArgsType = { + newDatasetName: string; + selectedLayer: APIDataLayer; + outputSegmentationLayerName?: string; + selectedBoundingBox: UserBoundingBox | null | undefined; +}; +type StartingJobModalProps = Props & { + jobApiCall: (arg0: JobApiCallArgsType) => Promise; + jobName: keyof typeof jobNameToImagePath; + description: React.ReactNode; + isBoundingBoxConfigurable?: boolean; + chooseSegmentationLayer?: boolean; + suggestedDatasetSuffix: string; + fixedSelectedLayer?: APIDataLayer | null | undefined; + title: string; + buttonLabel?: string | null; +}; + +type LayerSelectionProps = { + chooseSegmentationLayer: boolean; + layers: APIDataLayer[]; + tracing: HybridTracing; + fixedLayerName?: string; +}; + +export function getReadableNameOfVolumeLayer( + layer: APIDataLayer, + tracing: HybridTracing, +): string | null { + return "tracingId" in layer && layer.tracingId != null + ? getReadableNameByVolumeTracingId(tracing, layer.tracingId) + : null; +} + +export function LayerSelection({ + layers, + tracing, + fixedLayerName, + layerType, + onChange, + style, + value, +}: { + layers: APIDataLayer[]; + tracing: HybridTracing; + fixedLayerName?: string; + layerType?: string; + style?: React.CSSProperties; + // onChange and value should not be renamed, because these are the + // default property names for controlled antd FormItems. + onChange?: (a: string) => void; + value?: string | null; +}): JSX.Element { + const onSelect = onChange ? (layerName: string) => onChange(layerName) : undefined; + const maybeLayerType = layerType || ""; + const maybeSpace = layerType != null ? " " : ""; + return ( + + ); +} + +function LayerSelectionFormItem({ + chooseSegmentationLayer, + layers, + tracing, + fixedLayerName, +}: LayerSelectionProps): JSX.Element { + const layerType = chooseSegmentationLayer ? "segmentation" : "color"; + return ( + + ); +} + +type BoundingBoxSelectionProps = { + isBoundingBoxConfigurable?: boolean; + userBoundingBoxes: UserBoundingBox[]; + onChangeSelectedBoundingBox: (bBoxId: number | null) => void; + value: number | null; +}; + +function renderUserBoundingBox(bbox: UserBoundingBox | null | undefined) { + if (!bbox) { + return null; + } + + const upscaledColor = bbox.color.map((colorPart) => colorPart * 255) as any as Vector3; + const colorAsHexString = rgbToHex(upscaledColor); + return ( + <> +
+ {bbox.name} ({computeArrayFromBoundingBox(bbox.boundingBox).join(", ")}) + + ); +} + +export function BoundingBoxSelection({ + userBoundingBoxes, + setSelectedBoundingBoxId, + style, + value, +}: { + userBoundingBoxes: UserBoundingBox[]; + setSelectedBoundingBoxId?: (boundingBoxId: number | null) => void; + style?: React.CSSProperties; + value: number | null; +}): JSX.Element { + const onSelect = setSelectedBoundingBoxId + ? (boundingBoxId: number) => setSelectedBoundingBoxId(boundingBoxId) + : undefined; + return ( + + ); +} + +function BoundingBoxSelectionFormItem({ + isBoundingBoxConfigurable, + userBoundingBoxes, + onChangeSelectedBoundingBox, + value: selectedBoundingBoxId, +}: BoundingBoxSelectionProps): JSX.Element { + const dataset = useSelector((state: OxalisState) => state.dataset); + const colorLayer = getColorLayers(dataset)[0]; + const mag1 = colorLayer.resolutions[0]; + + return ( +
+

+ Please select the bounding box for which the inferral should be computed. Note that large + bounding boxes can take very long. You can create a new bounding box for the desired volume + with the bounding box tool in the toolbar at the top. The created bounding boxes will be + listed below. +

+ { + const selectedBoundingBox = userBoundingBoxes.find((bbox) => bbox.id === value); + if (selectedBoundingBox) { + const { isExportable, alerts: _ } = isBoundingBoxExportable( + selectedBoundingBox.boundingBox, + mag1, + ); + if (isExportable) return Promise.resolve(); + } + return Promise.reject(); + }, + message: `The volume of the selected bounding box is too large. The AI neuron segmentation trial is only supported for up to ${ + features().exportTiffMaxVolumeMVx + } Megavoxels. Additionally, no bounding box edge should be longer than ${ + features().exportTiffMaxEdgeLengthVx + }vx.`, + }, + ]} + hidden={!isBoundingBoxConfigurable} + > + + +
+ ); +} + +export function MagSlider({ + resolutionInfo, + value, + onChange, +}: { + resolutionInfo: ResolutionInfo; + value: Vector3; + onChange: (v: Vector3) => void; +}) { + // Use `getResolutionsWithIndices` because returns a sorted list + const allMags = resolutionInfo.getResolutionsWithIndices(); + + return ( + value.join("-"), + }} + min={0} + max={allMags.length - 1} + step={1} + value={clamp( + 0, + allMags.findIndex(([, v]) => V3.equals(v, value)), + allMags.length - 1, + )} + onChange={(value) => onChange(allMags[value][1])} + /> + ); +} + +type OutputSegmentationLayerNameProps = { + hasOutputSegmentationLayer: boolean; + notAllowedLayerNames: string[]; +}; + +export function OutputSegmentationLayerNameFormItem({ + hasOutputSegmentationLayer, + notAllowedLayerNames, +}: OutputSegmentationLayerNameProps) { + return ( + { + if (notAllowedLayerNames.includes(newOutputLayerName)) { + const reason = + "This name is already used by another segmentation layer of this dataset."; + return Promise.reject(reason); + } else { + return Promise.resolve(); + } + }, + }, + ]} + hidden={!hasOutputSegmentationLayer} + > + + + ); +} + +function StartingJobModal(props: StartingJobModalProps) { + const isBoundingBoxConfigurable = props.isBoundingBoxConfigurable || false; + const chooseSegmentationLayer = props.chooseSegmentationLayer || false; + const { handleClose, jobName, description, jobApiCall, fixedSelectedLayer, title } = props; + const [form] = Form.useForm(); + const userBoundingBoxes = useSelector((state: OxalisState) => + getUserBoundingBoxesFromState(state), + ); + const dataset = useSelector((state: OxalisState) => state.dataset); + const tracing = useSelector((state: OxalisState) => state.tracing); + const activeUser = useSelector((state: OxalisState) => state.activeUser); + const layers = chooseSegmentationLayer ? getSegmentationLayers(dataset) : getColorLayers(dataset); + const allLayers = getDataLayers(dataset); + + const startJob = async ({ + layerName, + boundingBoxId, + name: newDatasetName, + outputSegmentationLayerName, + }: { + layerName: string; + boundingBoxId: number; + name: string; + outputSegmentationLayerName: string; + }) => { + const selectedLayer = layers.find((layer) => layer.name === layerName); + const selectedBoundingBox = userBoundingBoxes.find((bbox) => bbox.id === boundingBoxId); + if ( + selectedLayer == null || + newDatasetName == null || + (isBoundingBoxConfigurable && selectedBoundingBox == null) + ) { + return; + } + + try { + await Model.ensureSavedState(); + const jobArgs: JobApiCallArgsType = { + outputSegmentationLayerName, + newDatasetName, + selectedLayer, + selectedBoundingBox, + }; + const apiJob = await jobApiCall(jobArgs); + + if (!apiJob) { + return; + } + + Toast.info( + <> + The {jobName} job has been started. See the{" "} + + Processing Jobs + {" "} + view under Administration for details on the progress of this job. + , + ); + handleClose(); + } catch (error) { + console.error(error); + Toast.error( + `The ${jobName} job could not be started. Please contact an administrator or look in the console for more details.`, + ); + handleClose(); + } + }; + + let initialLayerName = layers.length === 1 ? layers[0].name : null; + let initialOutputSegmentationLayerName = getReadableNameOfVolumeLayer(layers[0], tracing); + if (fixedSelectedLayer) { + initialLayerName = fixedSelectedLayer.name; + initialOutputSegmentationLayerName = getReadableNameOfVolumeLayer(fixedSelectedLayer, tracing); + } + initialOutputSegmentationLayerName = `${ + initialOutputSegmentationLayerName || "segmentation" + }_corrected`; + // TODO: Other jobs also have an output segmentation layer. The names for these jobs should also be configurable. + const hasOutputSegmentationLayer = jobName === JobNames.MATERIALIZE_VOLUME_ANNOTATION; + const notAllowedOutputLayerNames = allLayers + .filter((layer) => { + // Existing layer names may not be used for the output layer. The only exception + // is the name of the currently selected layer. This layer is the only one not + // copied over from the original dataset to the output dataset. + // Therefore, this name is available as the name for the output layer name. + // That is why that layer is filtered out here. + const currentSelectedVolumeLayerName = form.getFieldValue("layerName") || initialLayerName; + return ( + getReadableNameOfVolumeLayer(layer, tracing) !== currentSelectedVolumeLayerName && + layer.name !== currentSelectedVolumeLayerName + ); + }) + .map((layer) => getReadableNameOfVolumeLayer(layer, tracing) || layer.name); + + return ( + + {description} +
+ {jobNameToImagePath[jobName] != null ? ( + <> +
+ {`${jobName} +
+
+ + ) : null} +
+ + + + form.setFieldsValue({ boundingBoxId: bBoxId })} + value={form.getFieldValue("boundingBoxId")} + /> +
+ +
+ +
+ ); +} + +export function NucleiSegmentationModal({ handleClose }: Props) { + const dataset = useSelector((state: OxalisState) => state.dataset); + return ( + + startNucleiInferralJob( + dataset.owningOrganization, + dataset.name, + colorLayer.name, + newDatasetName, + ) + } + description={ + <> +

+ Start an AI background job to automatically detect and segment all nuclei in this + dataset. This AI will create a copy of this dataset containing all the detected nuclei + as a new segmentation layer. +

+

+ + Note that this feature is still experimental. Nuclei detection currently only works + with EM data and a resolution of approximately 200{ThinSpace}nm per voxel. The + segmentation process will automatically use the magnification that matches that + resolution best. + +

+ + } + /> + ); +} +export function NeuronSegmentationModal({ handleClose }: Props) { + const dataset = useSelector((state: OxalisState) => state.dataset); + return ( + { + if (!selectedBoundingBox) { + return Promise.resolve(); + } + + const bbox = computeArrayFromBoundingBox(selectedBoundingBox.boundingBox); + return startNeuronInferralJob( + dataset.owningOrganization, + dataset.name, + colorLayer.name, + bbox, + newDatasetName, + ); + }} + description={ + <> +

+ Start an AI background job that automatically detects and segments all neurons in this + dataset. The AI will create a copy of this dataset containing the new neuron + segmentation. +

+

+ + Note that this feature is still experimental and processing can take a long time. + Thus, we suggest to use a small bounding box and not the full dataset extent. The + neuron detection currently only works with EM data. The best resolution for the + process will be chosen automatically. + +

+ + } + /> + ); +} + +type MaterializeVolumeAnnotationModalProps = Props & { + selectedVolumeLayer?: APIDataLayer; +}; + +export function MaterializeVolumeAnnotationModal({ + selectedVolumeLayer, + handleClose, +}: MaterializeVolumeAnnotationModalProps) { + const dataset = useSelector((state: OxalisState) => state.dataset); + const tracing = useSelector((state: OxalisState) => state.tracing); + const activeSegmentationTracingLayer = useSelector(getActiveSegmentationTracingLayer); + const fixedSelectedLayer = selectedVolumeLayer || activeSegmentationTracingLayer; + const readableVolumeLayerName = + fixedSelectedLayer && getReadableNameOfVolumeLayer(fixedSelectedLayer, tracing); + const hasFallbackLayer = + fixedSelectedLayer && "tracingId" in fixedSelectedLayer + ? fixedSelectedLayer.fallbackLayer != null + : false; + const isMergerModeEnabled = useSelector( + (state: OxalisState) => state.temporaryConfiguration.isMergerModeEnabled, + ); + let description = ( +

+ Start a job that takes the current state of this volume annotation and materializes it into a + new dataset. + {hasFallbackLayer + ? ` All annotations done on the "${readableVolumeLayerName}" volume layer will be merged with the data of the fallback layer. ` + : null} + {isMergerModeEnabled + ? " Since the merger mode is currently active, the segments connected via skeleton nodes will be merged within the new output dataset. " + : " "} + Please enter the name of the output dataset and the output segmentation layer. +

+ ); + if (tracing.volumes.length === 0) { + description = ( +

+ Start a job that takes the current state of this merger mode tracing and materializes it + into a new dataset. Since the merger mode is currently active, the segments connected via + skeleton nodes will be merged within the new output dataset. Please enter the name of the + output dataset and the output segmentation layer. +

+ ); + } + + return ( + { + if (outputSegmentationLayerName == null) { + return Promise.resolve(); + } + const volumeLayerName = getReadableNameOfVolumeLayer(segmentationLayer, tracing); + const baseSegmentationName = getBaseSegmentationName(segmentationLayer); + return startMaterializingVolumeAnnotationJob( + dataset.owningOrganization, + dataset.name, + baseSegmentationName, + volumeLayerName, + newDatasetName, + outputSegmentationLayerName, + tracing.annotationId, + tracing.annotationType, + isMergerModeEnabled, + ); + }} + description={description} + /> + ); +} diff --git a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx index 1ab2c7728d..2ccbe5374d 100644 --- a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx @@ -48,7 +48,7 @@ import { updateUserSettingAction } from "oxalis/model/actions/settings_actions"; import { usePrevious, useKeyPress } from "libs/react_hooks"; import { userSettings } from "types/schemas/user_settings.schema"; import ButtonComponent from "oxalis/view/components/button_component"; -import { MaterializeVolumeAnnotationModal } from "oxalis/view/right-border-tabs/starting_job_modals"; +import { MaterializeVolumeAnnotationModal } from "oxalis/view/action-bar/starting_job_modals"; import { ToolsWithOverwriteCapabilities, AnnotationToolEnum, diff --git a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx index c453db37e7..a06fa5cefa 100644 --- a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx @@ -1,4 +1,4 @@ -import { Button, Dropdown, Modal, Tooltip } from "antd"; +import { Button, Dropdown, Modal, Radio, Tooltip } from "antd"; import { HistoryOutlined, CheckCircleOutlined, @@ -72,10 +72,7 @@ import UrlManager from "oxalis/controller/url_manager"; import { withAuthentication } from "admin/auth/authentication_modal"; import { PrivateLinksModal } from "./private_links_view"; import { ItemType, SubMenuType } from "antd/lib/menu/hooks/useItems"; -import { - NeuronSegmentationModal, - NucleiSegmentationModal, -} from "../right-border-tabs/starting_job_modals"; +import { NeuronSegmentationModal, NucleiSegmentationModal } from "./starting_job_modals"; const AsyncButtonWithAuthentication = withAuthentication( AsyncButton, @@ -261,38 +258,51 @@ export function getLayoutMenu(props: LayoutMenuProps): SubMenuType { export function getAISegmentationMenu( isAINucleiSegmentationModalOpen: boolean, isAINeuronSegmentationModalOpen: boolean, -): [SubMenuType, React.ReactNode] { - const AISegmentationMenu = { - key: "ai-segmentation-menu", - icon: , - label: "AI Segmentation", - children: [ - // { - // key: "ai-nuclei-segmentation", - // label: "AI Nuclei Segmentation", - // onClick: () => Store.dispatch(setAINucleiSegmentationModalVisibilityAction(true)), - // }, - { - key: "ai-neuron-segmentation", - label: "AI Neuron Segmentation", - onClick: () => Store.dispatch(setAINeuronSegmentationModalVisibilityAction(true)), - }, - ], - }; - - const AISegmentationModals = isAINucleiSegmentationModalOpen ? ( - Store.dispatch(setAINucleiSegmentationModalVisibilityAction(false))} - /> - ) : isAINeuronSegmentationModalOpen ? ( - Store.dispatch(setAINeuronSegmentationModalVisibilityAction(false))} - /> +): React.ReactNode { + return isAINeuronSegmentationModalOpen ? ( + + + Automated analysis with WEBKNOSSOS + + } + onCancel={() => Store.dispatch(setAINeuronSegmentationModalVisibilityAction(false))} + > + Choose a processing job for your dataset: + + +
+ {`Nuclei +
+
+ +
+ {`Nuclei +
+
+ +
+ {`Nuclei +
+
+
+ {/* insert modal here */} +
) : null; - - return [AISegmentationMenu, AISegmentationModals]; } class TracingActionsView extends React.PureComponent { @@ -675,11 +685,11 @@ class TracingActionsView extends React.PureComponent { } if (features().jobsEnabled) { - const [AISegmentationMenu, AISegmentationModals] = getAISegmentationMenu( + debugger; + const AISegmentationModals = getAISegmentationMenu( this.props.isAINucleiSegmentationModalOpen, this.props.isAINeuronSegmentationModalOpen, ); - menuItems.push(AISegmentationMenu); modals.push(AISegmentationModals); } diff --git a/frontend/javascripts/oxalis/view/action-bar/view_dataset_actions_view.tsx b/frontend/javascripts/oxalis/view/action-bar/view_dataset_actions_view.tsx index ac842e37e5..576f892511 100644 --- a/frontend/javascripts/oxalis/view/action-bar/view_dataset_actions_view.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/view_dataset_actions_view.tsx @@ -36,6 +36,7 @@ export default function ViewDatasetActionsView(props: Props) { const isAINucleiSegmentationModalOpen = useSelector( (state: OxalisState) => state.uiInformation.showAINucleiSegmentationModal, ); + debugger; const isAINeuronSegmentationModalOpen = useSelector( (state: OxalisState) => state.uiInformation.showAINeuronSegmentationModal, ); @@ -43,7 +44,7 @@ export default function ViewDatasetActionsView(props: Props) { (state: OxalisState) => state.uiInformation.showPythonClientModal, ); - const [AISegmentationMenu, AISegmentationModals] = getAISegmentationMenu( + const AISegmentationModals = getAISegmentationMenu( isAINucleiSegmentationModalOpen, isAINeuronSegmentationModalOpen, ); @@ -79,7 +80,6 @@ export default function ViewDatasetActionsView(props: Props) { icon: , label: "Download", }, - isAISegmentationEnabled ? AISegmentationMenu : null, props.layoutMenu, ], }; diff --git a/frontend/javascripts/oxalis/view/action_bar_view.tsx b/frontend/javascripts/oxalis/view/action_bar_view.tsx index 26c5a218b5..3070e12ffa 100644 --- a/frontend/javascripts/oxalis/view/action_bar_view.tsx +++ b/frontend/javascripts/oxalis/view/action_bar_view.tsx @@ -1,4 +1,4 @@ -import { Alert, Popover } from "antd"; +import { Alert, Popover, Tooltip } from "antd"; import { connect, useDispatch, useSelector } from "react-redux"; import * as React from "react"; import type { APIDataset, APIUser } from "types/api_flow_types"; @@ -36,6 +36,8 @@ import { setAdditionalCoordinatesAction } from "oxalis/model/actions/flycam_acti import { NumberSliderSetting } from "./components/setting_input_views"; import { ArbitraryVectorInput } from "libs/vector_input"; import { type AdditionalCoordinate } from "types/api_flow_types"; +import ButtonComponent from "./components/button_component"; +import { setAINeuronSegmentationModalVisibilityAction } from "oxalis/model/actions/ui_actions"; const VersionRestoreWarning = ( { location.href = `${location.origin}/annotations/${annotation.typ}/${annotation.id}${location.hash}`; }; + renderStartAIJobButton(): React.ReactNode { + return ( + Store.dispatch(setAINeuronSegmentationModalVisibilityAction(true))} + style={{ marginLeft: 12 }} + > + + AI Analysis + + + ); + } + renderStartTracingButton(): React.ReactNode { // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(props: AsyncButtonProps) => Ele... Remove this comment to see the full error message const ButtonWithAuthentication = withAuthentication(AsyncButton); @@ -249,6 +265,7 @@ class ActionBarView extends React.PureComponent { {isArbitrarySupported && !is2d ? : null} + {isViewMode ? null : this.renderStartAIJobButton()} {!isReadOnly && constants.MODES_PLANE.indexOf(viewMode) > -1 ? : null} {isViewMode ? this.renderStartTracingButton() : null}
diff --git a/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx b/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx index 58bae03a21..050ec7f4ab 100644 --- a/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx +++ b/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx @@ -110,7 +110,7 @@ import { settings, settingsTooltips, } from "messages"; -import { MaterializeVolumeAnnotationModal } from "oxalis/view/right-border-tabs/starting_job_modals"; +import { MaterializeVolumeAnnotationModal } from "oxalis/view/action-bar/starting_job_modals"; import AddVolumeLayerModal, { validateReadableLayerName } from "./modals/add_volume_layer_modal"; import DownsampleVolumeModal from "./modals/downsample_volume_modal"; import Histogram, { isHistogramSupported } from "./histogram_view"; diff --git a/public/images/mito_detection.png b/public/images/mito_detection.png new file mode 100644 index 0000000000..b06d27b08b Binary files /dev/null and b/public/images/mito_detection.png differ diff --git a/public/images/neuron_segmentation.png b/public/images/neuron_segmentation.png new file mode 100644 index 0000000000..d72603ea56 Binary files /dev/null and b/public/images/neuron_segmentation.png differ diff --git a/public/images/nuclei_inferral.png b/public/images/nuclei_inferral.png new file mode 100644 index 0000000000..458ab615c9 Binary files /dev/null and b/public/images/nuclei_inferral.png differ