VideoHelp Forum




+ Reply to Thread
Results 1 to 11 of 11
  1. I'm restoring old films and TV series and keep running into a recurring image quality issue: strong edges often show a purple band or halo. The artifact appears consistently around candlelight in night scenes, as well as along high-contrast edges in daylight. Here are a few examples

    Image
    [Attachment 92469 - Click to enlarge]

    Image
    [Attachment 92470 - Click to enlarge]

    Image
    [Attachment 92471 - Click to enlarge]

    Image
    [Attachment 92472 - Click to enlarge]

    Image
    [Attachment 92473 - Click to enlarge]

    Image
    [Attachment 92474 - Click to enlarge]


    I'm not entirely sure of the correct term – it may be related to “purple flare” or chromatic aberration. It’s very distracting and seriously degrades the viewing experience.

    Image
    [Attachment 92475 - Click to enlarge]

    Image
    [Attachment 92476 - Click to enlarge]


    The same clips frequently suffer from a related dark edge problem. For example, candle flames often show a dark line on one side and a purple fringe on the other – extremely annoying.

    I'm looking for an effective method/workflow that can remove these artifacts. I will provide a short test video; you only need to process the segments shown in the screenshots (no need to run the entire clip).

    test video clip
    https://drive.google.com/file/d/1kmni1sN9k-MgslyiIPKpLyqF2k-zTaMK/view?usp=sharing

    Key requirements:

    Minimize luma (Y) channel processing as much as possible, and avoid noticeable detail loss. Ideally, the Y channel should be left mostly untouched, but limited processing is acceptable if it doesn’t degrade fine details significantly. This step serves as a pre-processing module before an AI model handles detail restoration, so preserving image information is critical.

    Please note: the test source has been badly upscaled (not by me – I received it this way). You are free to downscale it if necessary.

    If your solution meets my requirements, I'll pay $50 USD. Please provide a clear workflow (script, filter chain, or detailed steps) that I can apply.
    Quote Quote  
  2. see: here, esp. #3 for a possible solution.
    Last edited by Selur; 30th May 2026 at 07:52.
    users currently on my ignore list: deadrats, Stears555, marcorocchini
    Quote Quote  
  3. Member VWestlife's Avatar
    Join Date
    Mar 2026
    Location
    New Jersey, USA
    Search PM
    Are you sure that's not just the inherent streaking/comet-tailing effect of a tube image sensor? You can't "restore" out of a video what was baked into it straight out of the camera, even before it hit the VTR. It's like trying to remove the lens flare effect you get when panning across the sun.

    I'd prefer to see the video as it was originally recorded, not digitally manipulated in an effort to make it look "more modern"... just my two cents, or I suppose rounded up to 5 cents these days, since they're not making pennies anymore.
    Quote Quote  
  4. There definitely is a school of thought that says you shouldn't alter what was in the media when it was shot. However, that means we should not remove gate weave from film; we shouldn't color correct a video or photo that was taken outdoors using indoor color settings; we shouldn't get rid of air conditioner hum in audio, even if it overwhelms the dialog; etc.

    Everyone has to draw their own line on what they want to change, but my criteria is simple: any change you make should make it more pleasant to watch.

    Also, if the "fix" makes the result look unnatural in any way, then I won't do it. That is why I am so far not running to AI. Every AI manipulation I've seen so far, including those done for "Hollywood" films, results in images that are artificial and vaguely unsettling.
    Quote Quote  
  5. Originally Posted by johnmeyer View Post
    There definitely is a school of thought that says you shouldn't alter what was in the media when it was shot. However, that means we should not remove gate weave from film; we shouldn't color correct a video or photo that was taken outdoors using indoor color settings; we shouldn't get rid of air conditioner hum in audio, even if it overwhelms the dialog; etc.

    Everyone has to draw their own line on what they want to change, but my criteria is simple: any change you make should make it more pleasant to watch.

    Also, if the "fix" makes the result look unnatural in any way, then I won't do it. That is why I am so far not running to AI. Every AI manipulation I've seen so far, including those done for "Hollywood" films, results in images that are artificial and vaguely unsettling.
    can't agree more!
    Quote Quote  
  6. Now increase the payment to 100 USD.
    Quote Quote  
  7. Seller's solution wasn't good enough to claim the reward?
    Quote Quote  
  8. As an additional improvement, if using machine learning filters is an option, adding '1x_BleedOut_Compact_300k_net_g' after the suggested solution through vsmlrt or vsgan additionally improves the color edges.
    (this is available through Hybrids vs-mlrt- and torch-add-ons)
    Additionally, BasicVSR++ for general chroma fixes, helps a lot with other chroma issues.
    (script, clip)
    Cu Selur
    Last edited by Selur; 6th Jun 2026 at 05:55.
    users currently on my ignore list: deadrats, Stears555, marcorocchini
    Quote Quote  
  9. Seems like my ideas were not what he was looking for. ¯\_(ツ)_/¯
    users currently on my ignore list: deadrats, Stears555, marcorocchini
    Quote Quote  
  10. Originally Posted by Selur View Post
    Seems like my ideas were not what he was looking for. ¯\_(ツ)_/¯
    Hi Selur, many thanks for your test, and sorry for the late reply. I've carefully checked your demo "withBasicVSR++onChroma.mp4".
    Yes, it works to remove the purple fringing, but at the same time, it also damages too many normal colors.

    Here's a sample:

    Image
    [Attachment 92711 - Click to enlarge]


    the colors of the girls' clothes and the flag become much duller. That cost is too high—this is unacceptable.
    Quote Quote  
  11. Here's a version which allows more control, has more documentation and changes the strength of the desaturation by different factors:
    Code:
    # ── Range definition ─────────────────────────────────────────────────────────
    # Each entry: (first_frame, last_frame, {param_overrides})
    # Leave the list empty to process the whole clip with the defaults above.
    ranges = [
        # (0, 99,  {"thr": 48, "desat": 0.3}),
        # (200, 350, {"mask_strength": 0.9, "dense_suppression": 0.4}),
    ]
    
    # ── Range examples ─────────────────────────────────────────────────────────────────────
    # Full clip, default settings (no ranges needed)
    ranges = []
    
    # One range with custom threshold and desaturation
    #ranges = [
    #   (100, 299, {"thr": 64, "desat": 0.2}),
    #]
    
    # Multiple non-overlapping ranges, each with independent overrides
    #ranges = [
    #   (0,   149, {"mask_strength": 0.4, "desat": 0.8}),
    #    (300, 599, {"thr": 16, "dense_suppression": 0.3, "desat": 0.1}),
    #]
    
    
    # ── Helpers ───────────────────────────────────────────────────────────────────
    def build_mask(src, bits, peak, mid,
                   thr, density_radius, falloff_radius,
                   mask_strength, dense_suppression, edge_curve):
        """Return (mask, desat_clip) for the given parameters."""
        edges = core.std.Prewitt(src.std.ShufflePlanes(0, vs.GRAY))
    
        edge    = core.std.Expr(edges, f"x {thr} - 0 max")
        density = core.std.BoxBlur(edge, hradius=density_radius, vradius=density_radius)
        density = core.std.Expr(density, f"x {peak} /")
        weight  = core.std.Expr(density, f"1 x -")
        weight  = core.std.Expr(weight,  f"x {dense_suppression} *")
        weight  = core.std.BoxBlur(weight, hradius=4, vradius=4)
    
        mask = core.std.Expr([edge, weight], "x y *")
        mask = core.std.BoxBlur(mask, hradius=falloff_radius, vradius=falloff_radius)
        mask = core.std.Expr(mask, f"x x * {peak} /")
        mask = core.std.Expr(mask, f"x {mask_strength} *")
        return mask
    
    
    def apply_desat(src, peak, mid, desat):
        return core.std.Expr(
            [src],
            expr=[
                "",
                f"x {mid} - {desat} * {mid} +",
                f"x {mid} - {desat} * {mid} +",
            ]
        )
    
    
    def process_segment(src, bits, peak, mid, params):
        """Apply the edge-desaturation effect with the given param dict."""
        p = params
        mask      = build_mask(src, bits, peak, mid,
                               p["thr"], p["density_radius"], p["falloff_radius"],
                               p["mask_strength"], p["dense_suppression"], p["edge_curve"])
        desat_clip = apply_desat(src, peak, mid, p["desat"])
        return core.std.MaskedMerge(src, desat_clip, mask)
    
    
    # ── Default parameter dict ────────────────────────────────────────────────────
    bits = clip.format.bits_per_sample
    peak = (1 << bits) - 1
    mid  = 1 << (bits - 1)
    
    defaults = dict(
        thr              = 32 << (bits - 8),
        density_radius   = 10,
        falloff_radius   = 6,
        mask_strength    = 0.6,
        dense_suppression= 0.65,
        edge_curve       = 2,
        desat            = 0.5,
    )
    
    # ── Apply ─────────────────────────────────────────────────────────────────────
    if not ranges:
        # No ranges defined → process entire clip with defaults
        clip = process_segment(clip, bits, peak, mid, defaults)
    else:
        # Build a frame-accurate splice
        # Start with the original clip; we'll replace segments one by one.
        n      = clip.num_frames
        result = clip   # baseline: unprocessed
    
        # Collect all range boundaries to build non-overlapping intervals
        # Strategy: trim → process → splice back with std.Splice / std.Trim
        # We accumulate replacements and rebuild the timeline in one pass.
    
        # Sort ranges by start frame
        sorted_ranges = sorted(ranges, key=lambda r: r[0])
    
        segments = []
        cursor   = 0
    
        for (first, last, overrides) in sorted_ranges:
            # Clamp to valid frame indices
            first = max(0, first)
            last  = min(n - 1, last)
    
            # Gap before this range (unprocessed)
            if cursor < first:
                segments.append(clip[cursor:first])
    
            # The range itself (processed with merged params)
            params  = {**defaults, **overrides}
            seg_src = clip[first:last + 1]
            segments.append(process_segment(seg_src, bits, peak, mid, params))
    
            cursor = last + 1
    
        # Remaining tail (unprocessed)
        if cursor < n:
            segments.append(clip[cursor:n])
    
        # Splice everything back together
        if len(segments) == 1:
            clip = segments[0]
        else:
            clip = segments[0]
            for seg in segments[1:]:
                clip = core.std.Splice([clip, seg])
    
    
    
    return [clip]
    you, could adjust the strength of the effect for specific ranges if need be.
    (imho developing something that would dynamically/magically adjust these values isn't worth it)

    Setup is the same as mentioned before.
    Here's link to a clip with uses the defaults for all scenes: https://www.mediafire.com/file/32zni6uskbilshb/default.mp4/file

    Cu Selur

    Ps.: personally, I would probably stick with the first version and just adjust the strength of the desaturation there, but this version offers more control.
    users currently on my ignore list: deadrats, Stears555, marcorocchini
    Quote Quote  



Similar Threads

Visit our sponsor! Try DVDFab and backup Blu-rays!