Summary
This is expected behavior. You can avoid it by using the alpha
channel for turbulence, not any of the color channels.
You can use feColorMatrix
to create greyscale turbulence from the alpha channel:
<filter id="turbulence-alpha" x="0" y="0" width="1" height="1">
<feTurbulence type="turbulence" baseFrequency="0.02" />
<feColorMatrix
values="0 0 0 1 0
0 0 0 1 0
0 0 0 1 0
0 0 0 0 1 " />
</filter>
But Why?
The behavior is surprising, but it matches the SVG specification and may be
correct for certain uses.
The unexpected lines are from the alpha channel, despite having discarded it!
For example, here are all four channels. Observe that the threads span all of
the color channels and matches right up with the near-zero portions of the
alpha channel.

(The SVG used to generate this is below under "Alpha Comparison SVG".)
The SVG specification (backup link) says:
Unless otherwise stated, all image filters operate on premultiplied RGBA
samples. Filters which work more naturally on non-premultiplied data
(feColorMatrix and feComponentTransfer) will temporarily undo and redo
premultiplication as specified.
"Premultiplied" in this case is talking about premultiplied alpha, where the
color channels are adjusted by the alpha. Premultiplication is good because it
allows compositing and filtering to work correctly. It happens behind the
scenes and you can ignore it... unless you modify the alpha channel.
The problem is that premultiplication loses data. And when alpha values
approach 0 (fully transparent), the data loss is particularly severe. When
feColorMatrix or feComponentTransfer "temporarily undo and redo
premultiplication", the undo operation is only an approximation. That data
loss manifests as unexpected lines throughout the image.
For example, given an input image whose color channels are

and whose alpha channel is

the premultiplied version of the color channels would be

Attemping to undo the premultipliction yields this:

There is damage throughout the image (just shy of 50% of the pixels mismatch),
but the difference from the original is most striking when the alpha is near zero.
(These images were created by the Python code below under "Comparison Image Generator". The premul_alpha
and unpremul_alpha
are based on
Inkscape's implementation)
What about type="fractalNoise"
?
All of the above applies to <feTurbulence type="fractalNoise">
, so why isn't it a problem?
Because <feTurbulence type="fractalNoise" numOctaves="1">
is raw Perlin 2d noise, and Perlin noise is in the range −0.707 through 0.707 (backup link). It's treated as a −1 through 1 range. Remapping that range to 0 through 255, all values end up between 37 through 217. The damage is present, but because the alpha is never close enough to 0, you don't see it.
It becomes visible with type="turbulence"
because Perlin turbulence uses the absolute value of the raw noise. So the range becomes 0.000 through 0.707, ultimately in the range 0 through 217. This is also why fractalNoise
doesn't have any pure black while turbulence
does (and why neither has any pure white).

(The source for this are in "Turbulence versus Noise" below.)
Footnotes
Alpha Comparison SVG
This is the SVG comparing the four channels emitted by feTurbulence
.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="230"
width="800"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<filter id="turbulence-red" x="0" y="0" width="1" height="1">
<feTurbulence type="turbulence" baseFrequency="0.02" />
<feColorMatrix
values="1 0 0 0 0
1 0 0 0 0
1 0 0 0 0
0 0 0 0 1 " />
</filter>
<filter id="turbulence-green" x="0" y="0" width="1" height="1">
<feTurbulence type="turbulence" baseFrequency="0.02" />
<feColorMatrix
values="0 1 0 0 0
0 1 0 0 0
0 1 0 0 0
0 0 0 0 1 " />
</filter>
<filter id="turbulence-blue" x="0" y="0" width="1" height="1">
<feTurbulence type="turbulence" baseFrequency="0.02" />
<feColorMatrix
values="0 0 1 0 0
0 0 1 0 0
0 0 1 0 0
0 0 0 0 1 " />
</filter>
<filter id="turbulence-alpha" x="0" y="0" width="1" height="1">
<feTurbulence type="turbulence" baseFrequency="0.02" />
<feColorMatrix
values="0 0 0 1 0
0 0 0 1 0
0 0 0 1 0
0 0 0 0 1 " />
</filter>
<text x="100" y="220" text-anchor="middle">Red Channel</text>
<rect x="0" y="0" width="200" height="200"
style="filter:url(#turbulence-red)" />
<text x="300" y="220" text-anchor="middle">Green Channel</text>
<rect x="200" y="0" width="200" height="200"
style="filter:url(#turbulence-green)" />
<text x="500" y="220" text-anchor="middle">Blue Channel</text>
<rect x="400" y="0" width="200" height="200"
style="filter:url(#turbulence-blue)" />
<text x="700" y="220" text-anchor="middle">Alpha Channel</text>
<rect x="600" y="0" width="200" height="200"
style="filter:url(#turbulence-alpha)" />
</svg>
Comparison Image Generator
This code generated the four square example images above.
#! /usr/bin/python3
from PIL import Image
def premul_alpha(color,alpha):
temp = alpha * color + 128
res = (temp + (temp >> 8)) >> 8
return res
def unpremul_alpha(color, alpha):
if alpha == 0: return color # Nonsensical operation
res = int((255 * color + alpha/2) / alpha)
return res
originalimg = Image.new("L",(256,256))
original_px = originalimg.load()
alphaimg = Image.new("L",(256,256))
alpha_px = alphaimg.load()
premulimg = Image.new("L",(256,256))
premul_px = premulimg.load()
restoredimg = Image.new("L",(256,256))
restored_px = restoredimg.load()
damagedimg = Image.new("L",(256,256),0)
damaged_px = damagedimg.load()
total = 0
dmg_count =0
for color in range(256):
for alpha in range(0,256):
original_px[color,alpha] = color;
alpha_px[color,alpha] = alpha;
during = premul_alpha(color,alpha)
premul_px[color,alpha] = during
restored = unpremul_alpha(during,alpha)
restored_px[color,alpha] = restored
total += 1
if restored != color:
dmg_count += 1
damaged_px[color,alpha] = 255
print(f"{dmg_count}/{total} -> {dmg_count/total}")
originalimg.save("original.png")
alphaimg.save("alpha.png")
premulimg.save("premul.png")
restoredimg.save("restored.png")
damagedimg.save("damaged.png")
Turbulence vs Noise
Noise:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="200"
width="200"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<filter id="turbulence-alpha" x="0" y="0" width="1" height="1">
<feTurbulence type="fractalNoise" baseFrequency="0.02" />
<feColorMatrix
values="0 0 0 1 0
0 0 0 1 0
0 0 0 1 0
0 0 0 0 1 " />
</filter>
<rect x="0" y="0" width="200" height="200"
style="filter:url(#turbulence-alpha)" />
</svg>
Turbulence:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="200"
width="200"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<filter id="turbulence-alpha" x="0" y="0" width="1" height="1">
<feTurbulence type="turbulence" baseFrequency="0.02" />
<feColorMatrix
values="0 0 0 1 0
0 0 0 1 0
0 0 0 1 0
0 0 0 0 1 " />
</filter>
<rect x="0" y="0" width="200" height="200"
style="filter:url(#turbulence-alpha)" />
</svg>