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=raiserefuses 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=falsegives 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
121497b6e24b92ff8b289552b2a830d89cff54908e50f068c241b58f3a5060f1
|
|
| MD5 |
4e6f39b2bbc0c660d5cd20b0148dc7ad
|
|
| BLAKE2b-256 |
d9d781b75dfa5ece7afc0a83566b203e7fabfe14b36c98f5953a151f7730bd69
|
Provenance
The following attestation bundles were made for pyscnslice-0.5.0.tar.gz:
Publisher:
release.yml on Jay2owe/PySCNSlice
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyscnslice-0.5.0.tar.gz -
Subject digest:
121497b6e24b92ff8b289552b2a830d89cff54908e50f068c241b58f3a5060f1 - Sigstore transparency entry: 2608851827
- Sigstore integration time:
-
Permalink:
Jay2owe/PySCNSlice@f951180b160a445f55ffcf2228d1b07ab67de295 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/Jay2owe
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f951180b160a445f55ffcf2228d1b07ab67de295 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d2aba18a09fffac65e67183fa54004cb8bd5ffcbcb46630865bd97935ce2205b
|
|
| MD5 |
99781775f1edfac36cbd5615c4b4eab3
|
|
| BLAKE2b-256 |
a44a3ff4ecba5d6860bdac3463697938e2907c37d02d66e5c83c15c2110ce124
|
Provenance
The following attestation bundles were made for pyscnslice-0.5.0-py3-none-any.whl:
Publisher:
release.yml on Jay2owe/PySCNSlice
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyscnslice-0.5.0-py3-none-any.whl -
Subject digest:
d2aba18a09fffac65e67183fa54004cb8bd5ffcbcb46630865bd97935ce2205b - Sigstore transparency entry: 2608852036
- Sigstore integration time:
-
Permalink:
Jay2owe/PySCNSlice@f951180b160a445f55ffcf2228d1b07ab67de295 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/Jay2owe
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f951180b160a445f55ffcf2228d1b07ab67de295 -
Trigger Event:
push
-
Statement type: