Skip to main content
Yanked

This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.

PySCNSlice

Automated analysis of bioluminescence and fluorescence time-lapse recordings from organotypic suprachiasmatic nucleus (SCN) slices.

The SCN is the master circadian pacemaker of the anterior hypothalamus. Kept alive as an organotypic slice and imaged for days, it reports its own timekeeping as a movie. PySCNSlice turns those movies into per-cell and whole-tissue rhythm measurements.

What is here

The first step, and the one that usually costs a person an afternoon with a mouse: finding the tissue. One call reads a registered recording, draws the accepted two-lobe outline, rotates it so both lobes sit the same way up in every recording, and writes a square crop centred on the outline — with no hand-drawn region of interest anywhere in the chain.

from pyscnslice import outline

result = outline.automatic(
    "meanred_MCG_04_1_595.tif",
    valid_mask="validfield_MCG_04_1_595.tif",
    output_dir="out",
)
result["output"]           # the two-lobe label image, oriented
result["cropped_output"]   # standard square crop around the SCN centre
result["report"]           # every setting, hash and measurement of that run

A registered ImageJ or OME hyperstack works as well as a two-dimensional time mean. scn_channel picks the outline channel and scn_z the depth plane, both using one-based ImageJ numbering; scn_time is "mean" (the default), "max", or a one-based frame number such as 320. Whichever plane is chosen determines one orientation and one crop, which are then applied to every plane in the stack. For a flattened RGB TIFF, scn_channel=1, 2, or 3 selects its red, green, or blue sample respectively. For a large online-only hyperstack, selected_source_only=True writes only the chosen two-dimensional plane and hash_source=False avoids downloading the rest merely to calculate a full-file hash; the selected plane and all outputs remain hashed. write_oriented_source=False keeps the orientation but skips writing the full oriented copy of the stack, which costs a second streaming pass over every plane and a second full-size file — wasted if the next step reads only the crop. The transform stays in the report either way.

A folder of recordings

from pyscnslice import batch

records = batch.run(
    "D:/recordings",                 # a folder, a file, or any list of paths
    output_root="D:/scn_out",        # each recording gets output_root/<stem>/
    scn_channel=1, scn_time="mean",  # shared by every recording
    per_source={"MCG_04_1_595": {"scn_channel": 2}},   # what differs
    workers=8,                       # recordings at once; unset chooses
    on_progress=lambda record: print(record["input"], record["ok"]),
)

workers is where a folder's wall-clock lives. The outline is single-threaded and around thirty seconds for a 512×512 field, against four milliseconds of file reading, so nothing about how the images are stored or passed in will move it. Left unset it is half the machine's cores, at most eight. A 27-recording library, measured on sixteen logical cores:

processes wall clock speed-up peak memory
1 434 s 1.00× 0.2 GB
2 196 s 2.21× 0.4 GB
4 141 s 3.07× 0.8 GB
8 90 s 4.83× 1.5 GB

The cap is memory, not speed: the gain is still real at eight, and each process holds its own recording, so a folder of stacks costs far more per process than the single frames measured there. Pass workers=1 for one process, or a bigger number on a machine with the memory for it.

On Windows a process pool needs the calling script to have an if __name__ == "__main__": guard — without one, each new process re-imports the script and runs it again. A run that chose its own worker count meets that by saying so and finishing in one process, so a script that worked before still works. A run that was told workers=8 fails loudly instead — every recording comes back carrying the error — because a caller who named a number is owed the news that it did not happen.

batch.run is a separate call rather than a list argument on outline.automatic, because the two want opposite things when something goes wrong: one recording should raise, a folder should finish and then tell you which three failed. Each record carries ok, skipped, the report path, and either the full single-call result or the error with its traceback; the same records are written to batch_scn_outline_manifest.json.

The same run from a shell:

python -m pyscnslice D:/recordings -o D:/scn_out --workers 8 --scn-channel 1
python -m pyscnslice D:/recordings --dry-run     # list what would be processed

Installing also puts a pyscnslice command on the path. It is argparse over batch.run and nothing else — every flag is one of that call's keywords, one for one. It prints a line per recording as it finishes and exits 0 when every recording produced output, 1 when any failed.

Discovery keeps the recordings and drops the valid-field masks, the partial writes and anything an earlier run wrote — from names alone, because opening a folder's worth of online-only files to read their metadata downloads the folder. A re-run skips any recording whose report already exists, so an interrupted folder resumes where it stopped; pass overwrite=True to redo the work.

Crops are "tight", "standard" (the default), "wide", or an exact crop_size_px. Every preset is checked to keep every outline pixel, and a custom size that would cut the outline is refused rather than silently clipped.

An instrument's folder, straight in

A PyIncucyte or PyLV200 download writes a folder of TIFFs and a manifest beside them. read_manifest turns that manifest into recordings, and outline_plan turns those into the two arguments batch.run already takes:

from pyscnslice import batch, outline_plan, read_manifest

recordings = read_manifest("D:/pull")        # either instrument, same records
sources, per_source = outline_plan(recordings, channel="red")
batch.run(sources, per_source=per_source, output_root="D:/scn_out")

channel takes a name — "red" also finds TRITC and mCherry, "phase" finds BF and brightfield — or a one-based number. The index it produces describes the stack as written, not the vessel, so a well that missed a channel does not shift every name after it by one.

No extra is needed to read a finished pull. A manifest is plain JSON, so the four dependencies above are enough. What a manifest says is worth looking at before a long run:

pyscnslice plan D:/pull
VID1234_A1_PhRd_20260820.tif  well A1  TCYX  393 frames  [Phase, Red]  -> channel 2

The whole sequence

pyscnslice pipeline D:/pull -o D:/scn_out --channel red
pyscnslice pipeline D:/pull --dry-run     # resolve every stage, run none
pyscnslice stages                         # what is installed on this machine

Thirteen stages, in order: acquire (PyIncucyte or PyLV200), index, trim_before_crop, broad_crop, trim, register, split, outline, image, grid, video, video_grid and trace (one call covering the cosmic-ray rule, a trace per region, the instrumental control and the rhythm verdict). Only the acquire stage needs another package, and the two movie stages need pyscnslice[video] for its encoder. The two trims and the split do nothing at all unless asked, so a run that does not mention them is the run it always was.

Each is reached by dotted name, so a stage whose package is not installed is reported as pending at the top of a run rather than raising four steps in; --allow-pending runs the rest. A stage is also reported pending when a library it imports once it is already running is missing — registration reaches for scikit-image inside its estimator, and a run that discovered that forty minutes in would be the exact failure this design exists to prevent.

Every stage appends what it did to pyscnslice-pipeline-run.json, written after each stage rather than at the end, because the stage that fails is the one whose record matters.

Six stages are opt-in. acquire runs only when an --experiment is given; without one the folder is taken as already pulled. broad_crop runs with --broad-crop, and rewrites every stack it crops, which is not something to do to a plate somebody already framed by hand. The other four are the display exports, below.

Pictures and movies out of the same run

pyscnslice pipeline D:/pull -o D:/scn_out --image --grid --video --video-grid
pyscnslice pipeline D:/pull -o D:/scn_out --grid-option moments=12 --grid-option columns=4
pyscnslice pipeline D:/pull -o D:/scn_out --video-option hours_per_second=24
Flag Makes Lands in
--image one still per recording visual/images/A1_mean.png
--grid every recording tiled into one sheet visual/images/grid_12moments.png
--video one movie per recording visual/videos/A1_24hps.mp4
--video-grid every recording playing at once in one movie visual/videos/grid_24hps.mp4

All four run after the outline and before the trace, so "index it, outline it and show me" is a complete run that never pays for the trace half. Files are named for the well and for what they are of, rather than for source stems that reach 149 characters by this point in the pipeline.

Every keyword of pyscnslice image, grid and video is reachable, as --<stage>-option KEY=VALUE, repeatable. Values are read as JSON where they parse as it, so moments=12 is a number and shared_range=false is false, and as plain text otherwise, so lut=fire works without a quoting puzzle. In Python it is one mapping per stage:

run_pipeline(folder, image=True, video_grid=True,
             image_options={"when": "max", "lut": "fire"},
             video_grid_options={"columns": 4, "hours_per_second": 24})

Giving a setting is itself the asking, so --grid-option moments=12 turns the stage on. A key the export will not take is refused by name before the run starts, checked against the function's own signature — misspelling colums costs a second rather than the outline stage's minutes.

Four things are filled in from what the run already knows, each only as a default that anything you pass overrides: where the file goes, what it is called, the frame interval from the recording's own metadata, and the well names on a sheet's or a mosaic's tiles.

The mosaic is the one with opinions, because tiling recordings that disagree is how a movie comes out looking right and being wrong:

  • Different lengths stop at the shortest, and the report names what it truncated. Holding a finished well's last frame while the others play on would show a dead recording still looking alive, which for a rhythm is a claim about the biology rather than a cosmetic choice. on_length=raise refuses instead.
  • Different frame intervals are refused by name. One clock is burned into one mosaic, so if frame k is half an hour into one well and a quarter of an hour into another that clock is wrong for every tile but one. Pass frame_interval_h= to state one for all of them.
  • Different display ranges are shared from the first recording by default, so a tile that looks brighter is brighter. shared_range=false gives each well its own, which is right for checking a layout and wrong for comparing wells.
  • Tiles of different sizes are padded, never resized, so a pixel means the same distance in every tile.

A plate is streamed, never assembled: one frame is painted from each recording, tiled, encoded and dropped before the next is read, so the memory is one row of frames rather than one row of stacks.

Nothing any of the four writes may be measured from. Each marks its output as a display artefact, the run record says so per stage, and none of them touches the recordings it was handed — a lookup-table painted 8-bit picture reaching the trace stage would be the one failure that looks like a result.

Analysing part of a recording

pyscnslice pipeline D:/pull -o D:/scn_out --trim 0..144h
pyscnslice pipeline D:/pull -o D:/scn_out --split-at 72h
pyscnslice pipeline D:/pull -o D:/scn_out --split baseline=0..70h --split drug=74h..144h

Both are said the same way: frames (1..288), hours (0..72h) or days (2d..5d), with .. between the two ends and either end left off to mean the beginning or the end. A time window is half-open, so 0..72h and 72h..144h are the two halves of a six-day recording and no frame is in both or in neither.

--trim 0..144h drops frames nothing should see. Use it when the microscope lost focus or the slice drifted out of the field: registration crops its export to the box every frame still covers, so a hundred bad frames at the end shrink the field of view for the whole recording, including the good days. It reruns registration, so it costs hours.

--split-at 72h produces two complete sets of outputs from one recording — traces/A1_start-72h/ and traces/A1_72h-end/ — measured through one registration, so the two are comparable. It reruns the outline and the trace, so it costs about a minute. Name the spans if you would rather read the folders later: --split baseline=0..70h --split drug=74h..144h, which also drops the four hours around a media change without leaving a gap inside either series.

A window said for one well beats the plate's, through the command that already exists for saying so:

pyscnslice correct D:/pull --stage trim --only B2 --value '"0..120h"'
pyscnslice correct D:/pull --stage split --only B2 --value '["0..48h", "48h.."]'

Moving a cut point writes new folders and leaves the old ones alone — nothing here deletes results. The run says which segment folders it did not write this time, so they are not read as current a month later.

--trim-before-crop puts the window on the stage before the broad crop instead of the one after it. It is for the recording whose bad frames fooled the crop detector, and it costs a full-frame copy of every recording it touches rather than a cropped one.

The broad crop

pyscnslice crop D:/pull --mode wide --dry-run   # how much would wide take off?
pyscnslice crop D:/pull -o D:/cropped --mode wide
pyscnslice crop D:/pull --when max              # draw the box from other frames
pyscnslice pipeline D:/pull --broad-crop --broad-crop-mode standard

--dry-run measures every recording, reports the box the chosen mode would draw, and writes nothing — worth running before committing a plate to a nine-day experiment. Without -o, crops go to AI_Exports/<stem>_broad_crop/ beside the source, which is where the folder form already knows not to look for inputs.

Registration costs what the frame costs. An Incucyte pull is 1152 × 1536 and the slice is about a fifth of it, so aligning the empty four fifths costs the same as aligning the tissue. This stage finds the tissue once and boxes it.

tight, standard and wide are 1.5, 2.0 and 2.8 times the box the tissue occupies — larger than the region crop's numbers because they scale the whole slice rather than the SCN outline, and because the surround they leave is what the accepted outline later reads its own background from. standard = 2.0 is not a taste: three Incucyte wells exist in both the full frame and a crop a person drew for this exact purpose, and those hand crops are 1.75 – 2.04 times the tissue box this finds.

--when picks which frames the box is drawn from, using the same three answers as --scn-time: mean, max, or a one-based frame number. Left alone it uses this stage's own rule — a median inside each of five chunks spread across the recording, then the maximum across them — which survives both a cosmic ray and the circadian trough while reading two dozen planes instead of all of them. Reach for --when when the default framed something you did not want; max holds the union of everywhere a drifting slice went, and a frame number is for the recording where you know which frame to look at.

It never cuts tissue, and it does not get tighter as the signal gets weaker. Otsu's threshold on the frame's own histogram says what is certainly tissue; a second, lower one says what is certainly background; and the box holds every connected piece that reaches the first while spreading out to the second. Both levels are read off the frame, so nothing has to be chosen in advance. Where the tissue fills the frame, or the well is empty, or the histogram cannot separate anything, it keeps the whole frame and the run record says why.

Four settings, three of them fractions of the frame and the fourth a compute budget — down from eleven, because Otsu now reads off the histogram what three of those settings used to have to be told.

single_object=False is the default and should stay off unless the stage did not move: the detection frame is a maximum across time chunks, so a slice that drifted shows as two blobs and the union of them is where the slice was. Turn it on for a large frame with bright debris elsewhere in the well. region=(x0, y0, x1, y1) crops to exactly what you name — obeyed, not judged, though the record still says whether it holds the tissue.

Measured across 75 real recordings spanning a 138-fold range of signal-to-noise ratio: 16 of the 75 cropped, none of the 16 hand-drawn outlines clipped, and the ten viral-reporter recordings that the first version of this stage was quietly cutting now keep every pixel of their tissue.

Drawing it yourself, or just saying it

pyscnslice pipeline D:/pull --broad-crop-rois D:/rois/crops    # the broad crop
pyscnslice pipeline D:/registered --stages outline     --outline-rois D:/rois/outlines                           # the SCN outline
pyscnslice crop D:/pull --rois D:/rois/crops --rois-if-missing raise
pyscnslice crop D:/pull --region 300,220,900,760              # one box, whole plate
pyscnslice D:/registered --angle 74 --crop-region 40,40,360,360
pyscnslice D:/registered --orient-rois D:/rois/up --outline-crop-rois D:/rois/squares

Four measured steps take the answer instead. Each has a folder of drawings and a value for the whole run, and they are separate options because they are cut from different frames:

step drawn said
broad crop --broad-crop-rois --broad-crop-region x0,y0,x1,y1
SCN outline --outline-rois
which way is up --orient-rois --angle DEG, --flip
square crop --outline-crop-rois --outline-crop-region x0,y0,x1,y1

Every flag says which crop, because pipeline runs two of them. There is no --crop-rois there: it used to mean the broad one, and a spelling that means a different stage in a different command writes a finished run in which nothing looks amiss. Typed anyway, it stops the run and names the four flags that do say which. Under pyscnslice <folder>, where the outline is the only stage, the square crop is also spelled --crop-rois and --crop-region, beside the --crop that already means it there.

Open the recording in Fiji, draw round the tissue with the freehand tool, save the region as a .roi or a ROI Manager RoiSet.zip named after the recording or its well, and point the run at the folder. A recording nobody drew is left to the automatic method, which is the point: a plate where three wells needed a person and ninety-three did not is the normal case. --rois-if-missing skip leaves the undrawn ones alone; raise stops the run, which is how a fully manual pass is asked for.

Which way is up is one line, not a region. Draw it from the ventral base to the dorsal tip with the straight-line tool: the direction of the stroke is the direction that ends up at the top. Analyze > Measure gives that stroke's angle, and typing it into --angle is the same instruction — 0 to the right, 90 straight up the screen. When the automatic rule found the axis and only called the dorsal end upside down, --flip is the whole correction. A per-recording angles.json{"B2": 74.5}, or {"B2": {"flip": true}} — says either for a plate.

Draw on what the step sees. An ImageJ ROI stores pixel coordinates and not the size of the image they were drawn on, so a region drawn on the raw recording and applied to a registered stack lands in the wrong place and nothing in the file says so. That is why the flags are separate: --broad-crop-rois is drawn on the raw recording, --outline-rois on the plane the outline reads — the *_OUTLINE_INPUT_*.tif a previous run wrote — and the square crop on the *_ORIENTED_SOURCE.tif, because that crop is cut after the rotation. A drawing that cannot fit inside the frame it is handed is refused rather than clamped into a plausible crop. A drawn direction is the exception: an angle has no position, so the same stroke means the same thing on either frame.

A given box is obeyed, not judged — and counted. The presets keep every outline pixel and --crop-size-px is refused if it would not, but a box you named is taken as given: the report carries outline_pixels_clipped and raises an open question rather than quietly returning a smaller SCN.

The outline is two lobes, so say which. Two regions is one lobe each; one region plus a line down the middle is the region cut along that line; one region alone leaves the midline to the accepted split, which runs inside the boundary you drew. Everything after the boundary is unchanged — the same orientation rule, the same square crop — and the report is written under its own tool name so nothing claims the frozen accepted geometry for an outline a person drew.

pip install "pyscnslice[incucyte]" adds the Incucyte download and pip install "pyscnslice[lv200]" the LV200 one; pip install "pyscnslice[rhythm]" adds what the trace stage needs to put a period on a trace.

Something to watch

pyscnslice video D:/pull --filters broad_crop,register,bioluminescence
pyscnslice video D:/pull/A1.tif --hours-per-second 6 --lut C1=green --lut C2=red
pyscnslice video-grid D:/pull --columns 3              # every well together
pyscnslice video D:/outlined --outline                 # plain + outlined copy
pyscnslice video --list-filters                 # the steps and their parameters

One movie per recording, and the filtering happens on the way through. --filters is a chain of this package's own steps, run over each channel in memory: steps separated by commas, parameters by colons, so register:downsample=2,unmix:coefficient=0.04 is a chain of two. A raw instrument pull goes straight to something watchable, and the filtered stack that used to sit between them — a gigabyte nobody wanted — is never written. --save-filtered keeps it for the run where you do.

step what it does
broad_crop box the tissue, drop the empty four fifths of the sensor
register take the drift out, keep the field every frame still covers
unmix subtract a scaled autofluorescence channel from this one
cosmic_rays replace pixels sitting above what the same pixel does either side
static_background the per-pixel whole-record mean out, band-limited, and back
bioluminescence suppress only what matches each pixel's own noise
smooth, local_contrast display smoothing, and each frame against its own blur

Every step calls the owning module's own function rather than a copy: a chained bioluminescence comes out bit-identical to display.bioluminescence_display's TIFF, and a test asserts it. The first two are geometry — they change how big the picture is — so they are planned once for the recording and every channel gets the same box.

Order is yours and it means something. broad_crop,register boxes the union of everywhere the slice went and registers the small frame — the pipeline's order, and the cheap one. register,broad_crop aligns the whole sensor first and boxes where the slice is. A filter either side is the same choice: filter the full frame, or filter the crop. The chain runs what is written down.

broad_crop takes the same --when the crop stage does: --filters broad_crop:when=max,register.

Nothing here may be measured from. Every movie records itself as a display artefact, and the one stack --save-filtered can write is marked _DISPLAY_ONLY whatever its chain was: it carries the movie's record rather than the unmixing coefficient's or the cosmic-ray rule's, so a number taken from it would have no artefact behind it. filtering.unmix and cosmic.remove_cosmic_rays are how a measurable stack is made.

A stack already marked _DISPLAY_ONLY can be cropped and registered — that is usually the one you most want held still to watch, and the movie is display-only either way. It still cannot be unmixed or de-spiked: that rule is about steps that change what a pixel means, and moving the picture is not one.

Playback is stated in experimental hours per second, not frames per second. At 24 — the default — one biological day takes one second of screen time whatever the acquisition interval was, and the frame rate is derived. No frame is ever dropped or duplicated to hit a rate.

video-grid makes one moving comparison sheet from several recordings. Its visual controls and defaults are the image grid's: each well name is on the left, time is centred below, gutters are white and 2.5% of a tile, and each recording gets one automatic range measured across its complete selected window. --well-label-orientation, --timestamp-position, --timestamp-format, --columns, --gap-px, --background, --tile-label and --own-range mean the same thing in both grid commands.

Every input contributes one frame to every output frame, so unequal selected frame counts are refused rather than silently truncated. Experimental-hours playback also needs one common acquisition interval. Use --frames to choose a common window and --fps when different cadences genuinely belong together; each tile's timestamp still follows its own recording.

Something to put in a figure

pyscnslice image D:/pull --filters broad_crop,register,bioluminescence
pyscnslice image D:/pull/A1.tif --when 412 --lut red    # one named frame
pyscnslice grid  D:/pull --moments 6                    # a time montage
pyscnslice grid  D:/pull --when mean                    # every well, one sheet

image is video with the encoder taken off, argument for argument — the same --filters, the same --lut, the same --display-range, because both draw through the same engine. A still made with a movie's arguments is that movie's frame, to the pixel.

--when is the whole difference:

--when the picture is
mean the window averaged. The default: a nine-day bioluminescence recording is mostly noise in any one frame
max the brightest each pixel ever got — and a hot pixel too, if the cosmic-ray rule has not run
412 recording frame 412, one-based, the same vocabulary --scn-time uses

A projection has its display range measured on itself, not on the frames behind it. The mean of a thousand frames has a lower maximum than any frame in it, so a range read off the frames would draw the mean too dark by exactly the amount the averaging removed. Its caption says a stretch rather than an instant — Time: 0 h 00 min - 240 h 00 min — because a mean happened at no single time.

grid tiles several of those into one file, and what varies decides what the sheet is:

tiles
--moments 12 one recording across time — a montage
a folder every recording at one moment — a contact sheet
a folder --moments 6 rows of recordings, columns of time

Time-course rows put each well name once on the left; use --well-label-orientation horizontal|vertical to turn those names. Times are centred below their tiles by default, with --timestamp-position top-left or top-right for corner text. --timestamp-format accepts hours, elapsed, clock, or a template such as {total_hours:.0f} h.

Tiles have white gutters 2.5% of their short side by default, following the Plot That image-grid reference. --gap-px and --background override them.

One display range for each complete recording or split. It is measured across the selected time window and applied to every time tile from that input, so a dim phase stays dim and a late bright phase cannot be clipped by an early tile. Automatic contrast uses the 99.999th percentile as its white point: it keeps biological peaks while remaining robust to an isolated extreme pixel. --own-range turns sharing off for a layout check.

A recording that will not open stops the sheet. --skip-bad finishes without it and prints what was left out, because a grid quietly missing three of its wells is worse than no grid.

A sheet is filed under the set of recordings that made it, and lands in that folder's own AI_Exports. Filing it under whichever recording was listed first is not just odd to read — add a well and the old sheet's key would still match, so a stale sheet would look current. A montage of a single recording is still filed under that recording, because there it really is a fact about it.

The still grid and video grid are display only on the same terms as each individual movie, and their records carry the same display_only mark.

Accepted outlines on display exports

Every image, movie and either kind of grid takes the same outline controls:

pyscnslice image D:/outlined --outline
pyscnslice grid D:/outlined --outline --outline-mode only
pyscnslice video A1.tif --outline A1_SCN_LABELS_ROI_CROP_STANDARD.tif
pyscnslice video-grid D:/pull --outline D:/analysis/batch_scn_outline_manifest.json

--outline without a path finds the accepted labels from the outline-stage source or batch_scn_outline_manifest.json. Python also accepts a label array, an ordered list for a grid, or a mapping keyed by source path, filename, stem or well name. The labels must already share the rendered image's orientation and crop; they are never resized into a plausible-looking but displaced boundary.

The default --outline-mode copy keeps the unchanged export and adds an *_outline copy. --outline-mode only writes just the outlined export at the requested name. The default boundary is opaque cyan and two pixels wide; --outline-colour, --outline-width-px and --outline-opacity change it.

The window

pyscnslice ui                                       # a window on this machine
pyscnslice ui --browser                             # the same page, in a browser
pyscnslice serve --host 0.0.0.0 --root D:/Imaging   # one machine, whole lab

A window, not a browser tab. Pick a folder — with the operating system's own dialog — see what is in it, start a run and watch the stages tick.

Underneath, the window is a loopback server drawing the same page serve serves. That is the reason it is not a desktop toolkit: Tkinter would give a window and take away hosting, and both were asked for. The window announces nothing and owns its server, so closing it stops it.

Every control on the page is a flag of pyscnslice pipeline with the same default, so nothing it can ask for is something a shell cannot — a test asserts that rather than a paragraph promising it. The one thing the window can do that the served page cannot is open that folder dialog, and a test pins the bridge to exactly that.

Two behaviours worth knowing. A run is submitted and watched rather than blocking, because registering ninety-six wells takes hours. And stopping takes effect at the next stage boundary — a stage is one call into another package, so "stop" honestly means "finish this one and do no more".

serve is the other half, and the only one with a host to worry about. There is no login, so binding to anything but loopback requires at least one --root, and every folder browsed, planned or run is checked against it.

$ pyscnslice serve --host 0.0.0.0
--host 0.0.0.0 serves this to the network, so it needs at least one --root
saying which folders it may read and write. Without one the folder picker would
offer the whole machine to anybody who can reach the port, and there is no login
to stop them.

The method is frozen

The outline is accepted Round 6 attempt 7 and the orientation accepted Round 9 attempt 6, from the Cry1-DIO-dLuc red-channel tuning project. The public settings retain the accepted pixel values, because six declared attempts to simplify or normalise them failed the truth, shape or generalisation gates — a shorter interface would have meant a less portable method, not a tidier one.

tests/test_automatic_scn_outline_parity.py compares output bytes against ten accepted fields rather than comparing behaviour. That evidence lives in the governed tuning project rather than in this repository; point PYSCNSLICE_TUNING_ROOT at it to run those tests, and without it they skip while the synthetic crop, orientation and input-safety tests still run.

Install

pip install pyscnslice

Only numpy, scipy, tifffile and scikit-image. No plotting stack, no web framework, no audit layer: outlining a slice should not install any of them. scikit-image is there because the accepted outline genuinely reaches it — convex_hull_image, inside the consensus that places the midline. It was declared under the register extra alone until 2026-08-25, which meant a plain install imported, read a manifest, and then failed partway through the first outline. tests/test_self_contained.py now runs the outline with everything undeclared blocked, which is the check that found it.

Eight extras, each needed only for what it names — and none of them to read a finished pull, because a manifest is plain JSON:

Extra For
pyscnslice[incucyte] starting an Incucyte acquisition
pyscnslice[lv200] pulling from a running LV200 (PyLV200)
pyscnslice[register] registration (OpenCV, scikit-image)
pyscnslice[image] stills, grids and colour maps (Pillow, matplotlib)
pyscnslice[video] movies too, which need an encoder (adds imageio-ffmpeg, imageio)
pyscnslice[rhythm] periods, cosinor fits and the rhythm verdict (circadian-workbench)
pyscnslice[ui] the page and the server behind it (FastAPI, uvicorn)
pyscnslice[desktop] the window (adds pywebview, which borrows the webview your machine already has)

Where this came from

These modules lived in PyMicroglia until 2026-08-23. Nothing about outlining a suprachiasmatic nucleus concerns microglia, and the code was already a leaf — nothing in that package imported it. PyMicroglia keeps the automatic_scn_outline action, which now delegates to pyscnslice.outline:

pip install "PyMicroglia[scn]"

That extra reaches PyPI with PyMicroglia's next release; the version published there today predates the move and has neither the extra nor the delegating module.

Where this is going

PySCNSlice is the SCN layer of an automated instrument-to-rhythm pipeline: download, crop broadly, register, outline and crop, trace, test the rhythm, render videos. The headless command is stage 3 of that plan, the window stage 4 and this release stage 5. docs/automated-scn-pipeline.md, in the repository, records which package owns which step and why.

License

MIT.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyscnslice-0.5.0.tar.gz (9.9 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyscnslice-0.5.0-py3-none-any.whl (566.0 kB view details)

Uploaded Python 3

File details

Details for the file pyscnslice-0.5.0.tar.gz.

File metadata

  • Download URL: pyscnslice-0.5.0.tar.gz
  • Upload date:
  • Size: 9.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pyscnslice-0.5.0.tar.gz
Algorithm Hash digest
SHA256 121497b6e24b92ff8b289552b2a830d89cff54908e50f068c241b58f3a5060f1
MD5 4e6f39b2bbc0c660d5cd20b0148dc7ad
BLAKE2b-256 d9d781b75dfa5ece7afc0a83566b203e7fabfe14b36c98f5953a151f7730bd69

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyscnslice-0.5.0.tar.gz:

Publisher: release.yml on Jay2owe/PySCNSlice

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyscnslice-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: pyscnslice-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 566.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pyscnslice-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d2aba18a09fffac65e67183fa54004cb8bd5ffcbcb46630865bd97935ce2205b
MD5 99781775f1edfac36cbd5615c4b4eab3
BLAKE2b-256 a44a3ff4ecba5d6860bdac3463697938e2907c37d02d66e5c83c15c2110ce124

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyscnslice-0.5.0-py3-none-any.whl:

Publisher: release.yml on Jay2owe/PySCNSlice

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 files

0.1.0

2 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page