Log in

View Full Version : ShiftLinesHorizontally not working as intended


Selur
24th April 2025, 15:22
A user got a clip (https://drive.google.com/drive/folders/10FlvHD87UNeTfmN9C8Ir1WvCEIWX0OrB) where I wanted to shift some lines horizontally.
So I wrote the following script:
import vapoursynth as vs
core = vs.core

def ShiftLinesHorizontally(clip: vs.VideoNode, shift: int, ymin: int, ymax: int) -> vs.VideoNode:
# Validate clip format and subsampling
if clip.format.color_family != vs.YUV or clip.format.subsampling_w != 0 or clip.format.subsampling_h != 0:
raise ValueError("ShiftLinesHorizontalRange: only YUV444 input is supported.")

# Ensure ymin and ymax are within valid range
if ymin < 0 or ymin >= clip.height:
raise ValueError(f"ShiftLinesHorizontalRange: ymin ({ymin}) is out of range.")
if ymax < ymin or ymax >= clip.height:
raise ValueError(f"ShiftLinesHorizontalRange: ymax ({ymax}) is out of range.")

# If no shift is needed, return original clip
if shift == 0:
return clip

width = clip.width
height = clip.height

# Create shifted version of just the target lines
mid = clip.std.CropAbs(width=width, height=ymax-ymin+1, left=0, top=ymin)
black = core.std.BlankClip(mid, width=abs(shift), height=mid.height, color=[0, 128, 128])

if shift > 0:
shifted_mid = core.std.StackHorizontal([black, mid.std.CropRel(right=shift)])
else:
shifted_mid = core.std.StackHorizontal([mid.std.CropRel(left=-shift), black])

shifted_mid = shifted_mid.resize.Point(width=width, height=mid.height)

# Build the output clip by stacking:
# 1. Lines above ymin (unchanged)
# 2. Shifted lines (ymin to ymax)
# 3. Lines below ymax (unchanged)
parts = []

if ymin > 0:
parts.append(clip.std.CropAbs(width=width, height=ymin, left=0, top=0))

parts.append(shifted_mid)

if ymax < height - 1:
parts.append(clip.std.CropAbs(width=width, height=height-ymax-1, left=0, top=ymax+1))

return core.std.StackVertical(parts)
and called it using:

import misc
clip = misc.ShiftLinesHorizontally(clip, shift=-24, ymin=468, ymax=479)

But something is wrong.
The 12 line I wanted are shifted (hurray), but the 12 lines above them got shifted too, and I don't see where I got it wrong.
https://i.ibb.co/LDb1G2nJ/grafik.png (https://ibb.co/ymGq3bWY)

=> does anybody see where my mistake is?

Cu Selur

_Al_
25th April 2025, 00:33
I don't know about posted script, but resize can shift nicely, positive or negative, then using overlay:

import vapoursynth as vs
from vapoursynth import core
import havsfunc
clip = ...

HORIZONTAL_SHIFT = 24
Y = 468
STRIP_HEIGHT = 12

shifted = clip.resize.Bicubic(src_left=HORIZONTAL_SHIFT)
masks = []
masks.append(core.std.BlankClip(clip, height=Y))
masks.append(core.std.BlankClip(clip, height=STRIP_HEIGHT, color=(255,128,128)))
try:
masks.append(core.std.BlankClip(clip, height=clip.height-(Y+STRIP_HEIGHT)))
except vs.Error:
pass
mask = core.std.StackVertical(masks)
clip = havsfunc.Overlay(base=clip, overlay=shifted, mask=mask)
clip.set_output()

Selur
25th April 2025, 11:15
Thanks, I'll try that tomorrow and report back. :)
(still interested in what I did wrong)

Cu Selur

Selur
25th April 2025, 11:51
using, whole script https://pastebin.com/rQgHyftP and like you suggested:
def ShiftLinesHorizontally(clip: vs.VideoNode, shift: int, ymin: int, ymax: int) -> vs.VideoNode:
# Validate clip format and subsampling
if clip.format.color_family != vs.YUV or clip.format.subsampling_w != 0 or clip.format.subsampling_h != 0:
raise ValueError("ShiftLinesHorizontalRange: only YUV444 input is supported.")

# Ensure ymin and ymax are within valid range
if ymin < 0 or ymin >= clip.height:
raise ValueError(f"ShiftLinesHorizontalRange: ymin ({ymin}) is out of range.")
if ymax < ymin or ymax >= clip.height:
raise ValueError(f"ShiftLinesHorizontalRange: ymax ({ymax}) is out of range.")

# If no shift is needed, return original clip
if shift == 0:
return clip
# shifted version
shift_height = ymax-ymin+1
shifted = clip.resize.Bicubic(src_left=shift)
# masking
masks = []
masks.append(core.std.BlankClip(clip, height=ymin))
masks.append(core.std.BlankClip(clip, height=shift_height, color=(255,128,128)))

try:
masks.append(core.std.BlankClip(clip, height=clip.height-(ymin+shift_height)))
except vs.Error:
pass
mask = core.std.StackVertical(masks)
clip = Overlay(base=clip, overlay=shifted, mask=mask, opacity=1)
return clip
I get the same effect:
https://i.ibb.co/8g4Ks0rx/grafik.png (https://ibb.co/VYpj9tgC)
(last few lines get shifted, but so do the lines above)

Cu Selur

Ps.: I also tried first saving a temp clip, after the deinterlacing, and using that as source, but the result is the same.
PPs.: Alternative link to source (https://drive.filejump.com/drive/s/fo5lsFMDWEuWgJFR8NqWmqY01ubYki).

StainlessS
25th April 2025, 14:27
I dont speak Parseltongue (Python/Vapoursynth), but your clip [TFF] seems to require different shift for even/odd field.
I would expect better result if doing field shifts prior to deinterlace.

I could not see any logic problems in your script [tryin' to understand logic], perhaps it is the differing shift that is causing/contributing to
your suspected problem. [shift after QTGMC deinterlace]

EDIT: [clickme to see independently shifted fields <not deinterlaced>]
https://i.postimg.cc/vgCn0m9b/i.jpg (https://postimg.cc/vgCn0m9b)

EDIT: Is there an Avisynth version of ShiftLinesHorizontally(), I did search but neither D9 nor Google (ShiftLinesHorizontally Site:forum.doom9.org)
found an equivalent (not even in this thread), feels like a JMac698 function.

Selur
25th April 2025, 15:48
The function is something I came up with.
I was planning to use this function in a broader context later, where I would shift different lines differently, but since my simple approach didn't work as expected, I assume I'm missing something obvious.

_Al_
26th April 2025, 01:38
I downloaded source. It is only bottom 6 lines, not 12. They wiggle left, right.
If using 12 lines, like you posted it shifts good 6 lines to the left.

Selur
26th April 2025, 09:03
AAAAARRRRGGGGH,... you are right. So easy. :)
Thanks! :)

Selur
26th April 2025, 12:12
Since I couldn't get the whole idea working with plain Vapoursynth I ended up using also numpy.
Script: https://pastebin.com/0qe12xVH
numpyhelper.py: https://pastebin.com/Zau8pGsj (can probably be improved to be faster => https://pastebin.com/zZP8Sxbe)
Not perfect, but way more stable:
https://i.ibb.co/VppphG4J/grafik.png (https://ibb.co/HpppMy8B)

Cu Selur

Chortos-2
26th April 2025, 19:49
FYI when you really do just want to shift some lines, you can do it in one line using akarin Expr (https://github.com/AkarinVS/vapoursynth-plugin):
clip.akarin.Expr('Y 476 >= x[24,0]:c x ?')

Selur
27th April 2025, 07:15
Nice. Thanks for the info!
So AutoFixLineBorders in numpyhelper could be rewritten to not use numpy? That would be nice and probably a lot faster!

Chortos-2
27th April 2025, 13:49
From a quick glance at the code, it seems to dynamically compute the amount of faded columns in each row, right? It’s not impossible to do that in Expr (for a fixed input resolution), but it wouldn’t be pretty, because Expr doesn’t have loops/jumps. NumPy is probably your best bet besides building a native-code plugin.

Selur
27th April 2025, 13:52
Yes, it checks every line. Thanks for the info, so for now it is numpy.