cutan.carve¶
Carve: a photo or a video frame in, a matted, normalised, provenance-carrying cut-out part out (cutan#10).
Every production that carves parts re-wrote the same pipeline by hand: locate → crop → matte → cut → de-spill → normalise → publish with provenance. This is that pipeline once, with the matte as a strategy seam, because no single method wins: a colour key beats a neural matte on flat cartoon frames, a neural matte wins on photos, GrabCut on a single prop in a rough box.
>>> import numpy as np
>>> frame = np.full((90, 120, 3), 245, np.uint8) # a flat backdrop
>>> frame[20:70, 30:90] = (40, 110, 200) # a prop on it
>>> part = carve(frame, point=(60, 45)) # flat_colour, the default
>>> part.size, part.anchor, part.quality.warnings()
((70, 60), (0.5, 0.5), [])
One part:
carve()(matte=a name fromMATTESor aMatte;Polygon(points) & FlatColour()is a hand outline cleanedby a key). A video frame comes from
grab_frame().A head:
carve_head()cuts the neck under the jaw and puts the face on a fixed canvas (512², the face centre as origin); the face is a hand box or found by a locator (insightface,pip install "cutan[faces]").Moving parts:
split_parts()lifts parts off a carving, each with a pivot (clock hands off the face), painting the base in under them.Provenance:
frame_source()(a licence is required, a YouTube URL gets its&t=deep link);write_prop()writesprop.json+parts/with the source in the descriptor, recipe and quality included, soan library publishgets the rights by construction.Quality: every
CarvingcarriesCarveQuality(coverage, backdrop left on the rim, how much de-spill repainted, a see-through interior, glyphs left attached or cut off, a subject cut off by the crop);quality.warnings()says which look wrong.Data, not code: every coordinate is in the source’s pixels, and a carving’s
recipeis its arguments by name, the matte as its recipe (as_mattereads one back):carve(frame, **part.recipe)replays it, so a batch of carves is a list of recipes.
Needs OpenCV: pip install "cutan[carve]" (check_requirements() lists
what is installed). Nothing here is imported by import cutan.
Functions
|
A |
|
The dominant colours of the image's border: the backdrop of a flat frame. |
|
Cut a part out of |
|
A head cut out at the neck and normalised onto a |
|
Which optional dependencies of carving are installed (printing how to get the rest). |
|
The provenance of a part carved from |
|
The frame of |
|
An |
|
A |
|
One face of several: |
|
Make a third-party strategy nameable ( |
|
Lift |
|
Write |
|
The video id of a YouTube URL, else |
Classes
|
Signals worth reading before a part is used (cutan#10). |
|
A carved part. |
|
Key out a saturated backdrop by its hue band (a stage drape, a green screen). |
|
A face in an image: its box and, when known, its jaw contour. |
|
Key out the backdrop's flat colours, where they are connected to the border. |
|
The sharp region of an image whose background is out of focus. |
|
OpenCV's GrabCut inside a box around one object. |
|
Where the subject is, in the pixels of the image the matte is given. |
|
Faces and their jaw contours from |
|
Re-map another matte's alpha: |
|
Base of the shipped strategies: a name, a recipe, and |
|
One lifted part: its RGBA image, its pivot in that image, where it sits in the carving. |
|
A carving split into a base ( |
|
What to lift off a carving: a |
|
A hand-given outline, |
|
|
Exceptions
An optional dependency of |
- class cutan.carve.CarveQuality(coverage, rim_backdrop_share, repainted_share, soft_interior_share, detached_pieces, attached_pieces, touches_edge)[source]¶
Bases:
objectSignals worth reading before a part is used (cutan#10).
coverage: the subject’s share of the carved region (near 0 or 1 means the matte took nothing or everything).rim_backdrop_share: the share of the part’s partly transparent edge whose colour is still close to the backdrop’s (a halo).repainted_share: the share of the subject whose colour de-spill replaced (high means it repainted real content: lowerdespill).soft_interior_share: the share of the subject’s interior (3 px or more inside its edge) that is see-through.detached_pieces: piecesdetachcut off;attached_pieces: pieces still hanging on the subject by a neck under 4 px (caption glyphs, or a genuinely thin part).touches_edge: the subject runs into the carved region’s border, so the crop probably cut it off.
- class cutan.carve.Carving(image, anchor, offset, scale, quality, recipe, source=None, meta=dict(), mode='part')[source]¶
Bases:
objectA carved part.
image: the RGBA part.anchor: its origin as(u, v)in 0..1 of the image (Pixi’s convention, anAttachment.anchor): the point for a plain carve, the face centre for a head.to_partmaps a source pixel into the part’s pixels (part = (source - offset) * scale; the 2×3 matrix ismeta["source_to_part"]).modeis"part"or"head";recipethe arguments it was made with (replayable:carve(image, **recipe)orcarve_head);sourcethe provenance;qualitythe signals;metamode-specific facts.- property alpha: ndarray¶
The alpha channel as floats in 0..1.
- class cutan.carve.Chroma(hue=None, width=60.0, min_sat=0.28, min_val=0.03, ramp=10.0)[source]¶
Bases:
MatteKey out a saturated backdrop by its hue band (a stage drape, a green screen).
hue:(low, high)in degrees (high < lowwraps through red);Nonecentres a bandwidthdegrees wide on the border’s median hue. A pixel is backdrop when its hue is in the band (rampdegrees soft at each end), its saturation abovemin_satand its value abovemin_val(each with a soft ramp), so dark and grey pixels of the subject stay.
- class cutan.carve.Face(box, jaw=None, score=1.0, landmarks=None)[source]¶
Bases:
objectA face in an image: its box and, when known, its jaw contour.
boxis(x0, y0, x1, y1);jawa sequence of(x, y)points along the jaw, ear to ear (any order, at least three);landmarksthe detector’s full set when there is one (kept, mapped onto the canvas, so a later step can place eyes and mouth without re-carving);scorethe detector’s confidence (1 for a hand box).>>> f = Face((10, 20, 50, 80)) >>> f.width, f.height, f.centre, f.chin_y (40.0, 60.0, (30.0, 50.0), 80.0)
- class cutan.carve.FlatColour(colours=None, tolerance=24.0, softness=16.0, edge=2.0, connected=True, seeds=None)[source]¶
Bases:
MatteKey out the backdrop’s flat colours, where they are connected to the border.
colours: the backdrop colours (RGB);Nonereads them off the image’s border (border_colours()). A pixel withintolerance(the largest per-channel difference) of one is backdrop, with asoftnessramp beyond it for anti-aliased edges — but only withinedgepx (source pixels) of the backdrop, so a subject colour a little off the backdrop’s (a pale grey on white) stays solid inside instead of turning see-through.connectedkeeps only backdrop regions that touch the image border (or theseeds), so a white interior enclosed by an outline (an eye, a shirt) stays in the subject.
- class cutan.carve.Focus(window=15)[source]¶
Bases:
MatteThe sharp region of an image whose background is out of focus.
Local detail (the Laplacian’s magnitude, averaged over
windowpx) is thresholded by Otsu’s method, closed, and its holes filled.
- class cutan.carve.GrabCut(box=None, iterations=8, margin=0.02, seed=0)[source]¶
Bases:
MatteOpenCV’s GrabCut inside a box around one object.
box:(x0, y0, x1, y1)in the SOURCE image;Nonetakes the hint’s box, else the whole image less amarginshare on each side.seedseeds OpenCV’s random generator before the cut, so a recorded recipe replays to the same matte.
- class cutan.carve.Hint(box=None, point=None, offset=(0.0, 0.0), scale=1.0)[source]¶
Bases:
objectWhere the subject is, in the pixels of the image the matte is given.
boxis(x0, y0, x1, y1)around the subject,pointone pixel on it.to_localmaps a point given in the SOURCE image (before the crop and the upscalecarve()applies) into these pixels, for strategies that take coordinates (a polygon, a GrabCut box).>>> Hint(offset=(100, 50), scale=2.0).to_local([(110, 60)]) [(20.0, 20.0)]
- class cutan.carve.InsightFaceLocator(*, model='buffalo_l', min_score=0.4, det_size=960)[source]¶
Bases:
objectFaces and their jaw contours from
insightface(106 landmarks; 0–32 are the jaw).min_scoredrops weak detections; the model (buffalo_l) is fetched by insightface on first use and kept for the process.
- class cutan.carve.Levels(of=None, lo=0.4, range=0.35)[source]¶
Bases:
MatteRe-map another matte’s alpha:
clip((alpha - lo) / range, 0, 1).Raising
lodrops a neural matte’s faint halo (the head carver’sedge_lo); not the same aschoke, which pulls the edge in by pixels.
- class cutan.carve.Matte[source]¶
Bases:
objectBase of the shipped strategies: a name, a recipe, and
&,|,~.recipe()is what provenance records about how a part was cut: the strategy’s name under"matte"and its constructor’s arguments (never the image), plain JSON. It is also data a carve can be replayed from:as_matte(m.recipe())builds the same matte.
- exception cutan.carve.MissingDependencyError[source]¶
Bases:
ImportErrorAn optional dependency of
cutan.carveis not installed.
- class cutan.carve.Part(name, image, pivot, origin, draw_order)[source]¶
Bases:
objectOne lifted part: its RGBA image, its pivot in that image, where it sits in the carving.
- class cutan.carve.PartSet(carving, base, parts=list())[source]¶
Bases:
objectA carving split into a base (
Nonewhen every pixel went to a part) and parts.
- class cutan.carve.PartSpec(mask, pivot, grow=1, reach=None)[source]¶
Bases:
objectWhat to lift off a carving: a
maskand thepivotit turns about.mask: a polygon[(x, y), ...]in source pixels, a matte called on the carving’s RGB with the carving’s frame as its hint (so a matte’s own coordinates are source pixels too;~FlatColour(colours=[grey], connected=False)selects the grey strokes), or an array of the carving’s size (bool or 0..1).pivot:(x, y)in source pixels.grow: carving px the mask is widened by before lifting (anti-aliased strokes).reach: keep only the mask’s pieces that come within this many source px of the pivot (a colour key also matches specks elsewhere: a tick’s grey edge).
- class cutan.carve.Polygon(points=(), supersample=4)[source]¶
Bases:
MatteA hand-given outline,
[(x, y), ...]in the SOURCE image, anti-aliased.
- class cutan.carve.Rembg(model='isnet-general-use', post_process=True)[source]¶
Bases:
Matterembg’s neural background removal (pip install "cutan[rembg]").model: a rembg model name (isnet-general-usefor photos and faces,isnet-animefor drawn figures,u2net_human_segfor people). The model is downloaded by rembg on first use and its session kept for the process.
- cutan.carve.as_matte(matte)[source]¶
A
Mattefrom a strategy’s name, its recipe, aMatte, or any(rgb, hint) -> alphacallable.A recipe (
Matte.recipe(), as a carve records it) builds the same matte back, so a strategy with its settings is plain data (a batch spec, a CLI):>>> as_matte("chroma").name 'chroma' >>> m = as_matte({"matte": "and", "of": [{"matte": "chroma", "width": 40}, ... {"matte": "polygon", "points": [[0, 0], [9, 0], [9, 9]]}]}) >>> m.recipe() == as_matte(m.recipe()).recipe(), m.recipe()["of"][0]["width"] (True, 40) >>> as_matte("rainbow") Traceback (most recent call last): ... ValueError: no matte strategy 'rainbow'; the strategies are: chroma, flat_colour, ...- Return type:
- cutan.carve.border_colours(rgb, *, max_colours=3, min_share=0.08, bits=4)[source]¶
The dominant colours of the image’s border: the backdrop of a flat frame.
Border pixels are binned at
bitsper channel; every bin holding at leastmin_shareof the border (up tomax_colours, most frequent first) gives one colour, the median of its pixels.>>> img = np.zeros((20, 20, 3), np.uint8); img[:] = (250, 240, 230) >>> border_colours(img) [(250, 240, 230)]
- cutan.carve.carve(image, *, matte='flat_colour', crop=None, box=None, point=None, keep='point', fill_holes=True, detach=0, choke=0, feather=0.6, despill=1, upscale=1, pad=4, source=None)[source]¶
Cut a part out of
image; every coordinate is in the source image’s pixels.image: a path, a PIL image or an RGB(A) array (
grab_framefor video) matte: the strategy (cutan.carve.mattes): a name, a recipe mapping,a
Matte(Polygon(pts) & FlatColour()) or any(rgb, hint) -> alphacrop:
(x0, y0, x1, y1), the region carved (default: the whole image) box: a box around the subject (GrabCut’s default rectangle) point: a pixel on the subject;keep="point"keeps its component, and itbecomes the part’s anchor (default: the part’s centre)
- keep:
"point"(its component and pieces within a few px),"largest" or
"all"components
fill_holes: fill what the subject encloses (a pale face the matte missed) detach: cut off pieces hanging on the subject by a neck narrower than
2 * detachpx (caption letters; also any part that thin); 0 keeps them and reports themchoke: pull the edge in by this many px, then
feathersoftens it (px) despill: re-paint the edge from the subject’s pixels at least this many pxinside it (1: the anti-aliased band; about 4 for a neural matte’s fuzzy fringe; 0: off)
- upscale: matte at this whole factor of the source resolution, then come
back: smoother edges on small or low-resolution subjects
pad: transparent px left around the trimmed part source: provenance (
frame_source()), keptwith the recipe and the quality in
Carving.provenance()>>> img = np.full((60, 80, 3), 250, np.uint8); img[15:45, 20:60] = (30, 90, 200) >>> part = carve(img) >>> part.size, round(part.quality.coverage, 2), part.quality.warnings() ((50, 40), 0.25, []) >>> carve(img, **part.recipe).recipe == part.recipe # the recipe replays the carve True- Return type:
- keep:
- cutan.carve.carve_head(image, *, face=None, locate=None, pick='largest', matte='flat_colour', cut='auto', neck=None, neck_soft=0.05, pad_x=1.05, pad_top=1.25, pad_bottom=1.0, upscale=3, keep='point', fill_holes=True, detach=0, choke=0, feather=0.6, despill=1, canvas=512, face_h=270.0, centre=(256.0, 300.0), margin=6, view=None, source=None)[source]¶
A head cut out at the neck and normalised onto a
canvas-px square.- face: the face, a
Face(box and, when known, a jaw contour and landmarks), a box
(x0, y0, x1, y1)or a mapping (Face.as_dict());Nonerunslocate- locate: a face locator
rgb -> [Face](default InsightFaceLocator,cutan[faces]);pickchooses among several faces- cut: the neck cut,
"auto"(along the jaw when the face has a contour, else flat under the chin),
"jaw","flat"or"none";neckits depth in face-box heights,neck_softits soft edge- pad_x, pad_top, pad_bottom: the crop around the face, in face-box widths
(each side) and heights (above, below)
upscale: matte at this whole factor (heads are small in a frame) face_h, centre: the face box is scaled to
face_hpx high and itscentre put on
centre, unless the head would then leave the canvas (lessmargin), in which case it is scaled down to fit- view: the view the head is drawn in, one of the rig’s views (
front, three_quarter,side,back), recorded for the step that puts it on a rig;Nonewhen unknown
The rest are
carve()’s (despillin source px: a neural matte on a photo wants about 4). The recipe pins the face, so a replay is reproducible whether it was given or located (meta["located_by"]says which); a batch spec is{**shared_settings, **per_item}(the face and the crop per image, the matte and the finish shared). The part’s anchor iscentre(the face centre);metaholds what a rig needs to hang it: the face box, the chin and the neck point, the head’s bounding box, the landmarks, all on the canvas.- Return type:
- face: the face, a
- cutan.carve.check_requirements(*, verbose=True)[source]¶
Which optional dependencies of carving are installed (printing how to get the rest).
>>> sorted(check_requirements(verbose=False)) ['cv2', 'insightface', 'rembg']
- cutan.carve.frame_source(url, *, license, t=None, author=None, author_url=None, title=None, provider=None, attribution=None, note=None, cacheable=True, **extra)[source]¶
The provenance of a part carved from
url(a video at timet, or an image).licenseis required (keyword-only, no default):Nonerecords the rights as UNKNOWN, which is a statement too. For footage you do not own,"all-rights-reserved"(an creditsthen lists the part as NOT PUBLISHABLE). A YouTube URL is normalised to its watch page (tracking parameters dropped) with a&t=deep link;tdefaults to the URL’s own. Any other URL keeps its scheme, host and path only (no password, no query: signed-URL tokens are credentials). A local file (a path, afile:URL) is recorded by its name only: providerlocal, the name inextra.file, no path anywhere; adata:URI is refused.t,titleandnotego intoextrabeside any other keyword.>>> s = frame_source("https://www.youtube.com/watch?v=AAGIi62-sAU&si=track", ... t=34.6, license="all-rights-reserved", author="OverSimplified") >>> s.provider, s.id, s.url ('youtube', 'AAGIi62-sAU', 'https://www.youtube.com/watch?v=AAGIi62-sAU&t=34') >>> s.source_page_url, s.extra["frame_time_s"] ('https://www.youtube.com/watch?v=AAGIi62-sAU', 34.6) >>> frame_source("https://youtu.be/AAGIi62-sAU?t=1m30s", license=None).extra["frame_time_s"] 90.0 >>> local = frame_source("/home/me/private/clip.mp4", t=3, license="all-rights-reserved") >>> local.provider, local.url, local.extra["file"], "/home" in local.attribution ('local', None, 'clip.mp4', False)- Return type:
AssetSource
- cutan.carve.grab_frame(video, t, *, ffmpeg='ffmpeg')[source]¶
The frame of
videoattseconds, as RGB (needs theffmpegbinary).- Return type:
ndarray
- cutan.carve.load_rgb(image)[source]¶
An
HxWx3uint8RGB array from a path, a PIL image or an array.>>> load_rgb(np.zeros((4, 5, 4), np.uint8)).shape (4, 5, 3)- Return type:
ndarray
- cutan.carve.neck_cut(shape, face, *, cut='auto', neck=None, soft=0.05, offset=(0.0, 0.0), scale=1.0)[source]¶
A
shapemask, 1 above the neck cut and 0 below, with a soft edge.cut:"jaw"follows the jaw contour (ends extended past the ears, deeper at the chin),"flat"cuts straight across under the chin,"auto"isjawwhen the face has a contour andflatotherwise, and"none"keeps everything.neckis the depth below the jaw (or the chin) in face-box heights.offset/scalemap the face (given in source pixels) into the mask’s pixels.>>> m = neck_cut((100, 50), Face((10, 10, 40, 60)), cut="flat", neck=0.1) >>> float(m[50, 25]), float(m[90, 25]) (1.0, 0.0)- Return type:
ndarray
- cutan.carve.pick_face(faces, pick='largest')[source]¶
One face of several:
largest,leftmost,rightmostorbest(score).>>> pick_face([Face((0, 0, 10, 10)), Face((50, 0, 70, 20))]).box (50.0, 0.0, 70.0, 20.0)- Return type:
- cutan.carve.register_matte(cls, *, name=None)[source]¶
Make a third-party strategy nameable (
matte="<name>") and its recipes replayable:register_matte(SamMatte)(or as a class decorator). The name is the class’sname; registering a different class under a taken name is refused, so a recorded recipe never changes meaning.
- cutan.carve.split_parts(carving, parts, *, paint_out=True, inpaint_radius=4, pad=2)[source]¶
Lift
parts({name: PartSpec}, in draw order) offcarving.Each part takes the carving’s pixels under its mask. Parts are drawn in the order listed, the last on top, and the one drawn on top owns a pixel two masks share (a clock’s hub cap, listed after the hands, keeps the hub). The base keeps the rest, and stays opaque under a part’s soft edge (no seam at rest); where a part was enclosed by the base,
paint_outre-paints the base under it (cv2.inpaint) so it stays whole when the part moves. A part’soriginandpivotare in the carving’s pixels (wherewrite_prop()places it).- Return type:
- cutan.carve.write_prop(carved, folder, *, name=None, unit=None, max_side=None, display_name=None, source=None, ours=False, relicense=None, overwrite=False)[source]¶
Write
carvedtofolderasprop.json+parts/*.png; return the descriptor.name: the prop’s name (default: the folder’s name) unit: view_box units per carved pixel (default 1); or
max_side: theunit that makes the longer side that many units (never above 1)
- source: overrides the carving’s provenance (its recipe and quality are
still recorded in
extra.carve, its source inextra.carved_from)
ours: the pixels are the caller’s own art (a carving with no source) relicense:
{"by": who, "reason": why}(both non-empty), required for asourcethat loosens the carving’s licence class. Recorded in the descriptor’ssource.extra.relicensed;an library publishdoes not read it, so say it again there (relicense=) when publishing.A
PartSetis written with its carving’s provenance: build one withsplit_parts()(its parts are cut from that carving), never by hand from other carvings’ parts.The root bone sits at the carving’s anchor. A split prop gets one bone per part, at its pivot, parented to the root, and one slot per part above the base, in the order the parts were listed. Nothing is written unless the whole descriptor is valid.
- Return type:
PropDescriptor
- cutan.carve.youtube_id(url)[source]¶
The video id of a YouTube URL, else
None.>>> youtube_id("https://www.youtube.com/watch?v=AAGIi62-sAU&t=34") 'AAGIi62-sAU' >>> youtube_id("https://youtu.be/AAGIi62-sAU?t=3"), youtube_id("https://example.org/a.png") ('AAGIi62-sAU', None) >>> youtube_id("https://notyoutube.com/watch?v=x"), youtube_id("https://WWW.YOUTUBE.COM:443/shorts/abc") (None, 'abc')
Modules
The carve pipeline: an image (or a video frame) in, a matted, cleaned, provenance-carrying part out. |
|
Head mode: a face located, the neck cut under the jaw, the head normalised on a fixed canvas. |
|
Matte strategies: which pixels of an image are the subject. |
|
Split a carved prop into moving parts, each with a declared pivot. |
|
Write a carving (or a carving split into parts) as a library-ready prop folder. |
|
Where carved pixels came from: an |
|
Clean a raw matte into a cut-out's alpha: keep the subject, close it, edge it, de-spill it. |