BiSeNet or XSeg? Choosing the Right Face Mask for an Image Pipeline A developer adapted a BiSeNet-versus-XSeg face-parsing comparison into an implementation guide for the UniFace library, showing that BiSeNet returns a 19-class integer label map for region selection while XSeg returns a single floating-point face mask aligned to full-image coordinates. The guide provides working Python code for both paths, including crop-margin handling and the XSeg.parse_aligned() variant, and notes that in a three-face sample the larger BiSeNet backbone did not consistently preserve more detail and XSeg produced an almost binary mask. A face parser can return a map of facial parts or a mask of the face as a whole. Those outputs can look similar in a visualization, but they support different operations. If an editor needs to select hair or lips, it needs region labels. If a compositing step needs a face-shaped selection, a single mask may be sufficient. Choosing between BiSeNet and XSeg starts with that distinction, before comparing model size or inference time. This article adapts my original BiSeNet/XSeg comparison https://yakhyo.github.io/blog/2026/09/bisenet-vs-xseg-face-parsing/ into an implementation guide for UniFace. It retains the output examples while separating API behavior from observations on a small set of portraits. | Property | BiSeNet in UniFace | XSeg in UniFace | |---|---|---| | Output | Integer class map, IDs 0–18 | Floating-point mask, values 0–1 | | Selection | Individual regions such as hair, skin, and lips | A face region, without separate part labels | | Input to the normal parsing call | Face-centered BGR crop | BGR image and five alignment landmarks | | Output coordinates | Same dimensions as the supplied crop | Warped back to the supplied image dimensions | BiSeNet's 19 classes include background, skin, eyes, eyebrows, nose, lips, and hair. A selection is a comparison against one or more class IDs. XSeg supplies one mask; it cannot tell the application which selected pixels are lips or skin. In this example, XSeg's mask excludes the hair. It should not be treated as a full portrait or head mask. If the task is separating a person, including hair, from a background, portrait matting is a separate option. A mask is only useful if it lines up with the pixels being edited. BiSeNet returns labels in the coordinate system of its input crop. XSeg's normal parsing call aligns the face using landmarks, runs inference, then transforms the result back into the original image. The following example makes those two coordinate systems explicit. Install UniFace with the CPU runtime using pip install "uniface cpu " and supply a portrait containing one face. python import cv2 import numpy as np from uniface import BiSeNet, RetinaFace, XSeg image = cv2.imread 'portrait.jpg' if image is None: raise FileNotFoundError 'Could not read portrait.jpg' faces = RetinaFace .detect image if len faces = 1: raise ValueError f'Expected one face, found {len faces }' face = faces 0 Expand the detector box to retain more hair and surrounding context. x1, y1, x2, y2 = face.bbox :4 bw, bh = x2 - x1, y2 - y1 height, width = image.shape :2 left = max 0, int np.floor x1 - 0.2 bw top = max 0, int np.floor y1 - 0.4 bh right = min width, int np.ceil x2 + 0.2 bw bottom = min height, int np.ceil y2 + 0.2 bh crop = image top:bottom, left:right labels = BiSeNet .parse crop hair in crop = labels == 17 .astype np.uint8 255 cv2.imwrite 'hair crop.png', hair in crop face mask = XSeg .parse image, landmarks=face.landmarks cv2.imwrite 'face mask.png', face mask 255 .astype np.uint8 The crop margins are a preprocessing choice, not a guarantee that every hairstyle fits. The saved hair mask belongs to the expanded crop. To position it in the full frame, use the recorded left and top offsets. The XSeg mask already has the full image dimensions. UniFace also exposes XSeg.parse aligned for callers that already have an aligned crop. That path returns a mask in crop coordinates and does not perform the full-image transformation. The original comparison examined three faces. In that small sample, the larger BiSeNet backbone did not consistently preserve more detail, and XSeg produced an almost binary mask despite its floating-point output. These are observations about those examples. They do not establish a general backbone ranking or a minimum usable face size. Counting which classes appear also does not measure whether the boundaries are correct; that requires reference annotations and a segmentation metric. This occluded portrait illustrates another evaluation question: does the mask include the obstruction? The example is worth inspecting, but one scarf image does not establish performance on hands, glasses, or other occlusions. For region-specific editing, evaluate the classes the editor actually uses and inspect boundaries at the intended image resolution. For face compositing, examine mask coverage, alignment errors, and leakage into hair or foreground objects. A value between zero and one is not evidence of physically accurate transparency or a calibrated probability. Smoothing a face mask can soften an edge; it does not recover hair detail or turn the output into a portrait alpha matte. BiSeNet and XSeg provide different selections. The useful comparison is whether each output supports the next image operation with acceptable errors on representative inputs. See the parsing API documentation https://yakhyo.github.io/uniface/modules/parsing/ , the BiSeNet implementation and weights https://github.com/yakhyo/face-parsing , and the XSeg ONNX repository https://github.com/yakhyo/face-segmentation for implementation details. Check the model licence terms https://yakhyo.github.io/uniface/license-attribution/ before deployment.