Repository: pydicom/pydicom

Issue:
Segmented LUTs are incorrectly expanded
**Describe the bug**
`pydicom.pixel_data_handlers.util._expand_segmented_lut()` expands segmented LUTs to an incorrect length.

**Expected behavior**
A correct length LUT to be produced.

**Steps To Reproduce**
Initialize the following variables.
```
import numpy as np
length = 48
y0 = 163
y1 = 255
```

Run the following two lines from [`pydicom.pixel_data_handlers.util._expand_segmented_lut()`](https://github.com/pydicom/pydicom/blob/699c9f0a8e190d463dd828822106250523d38154/pydicom/pixel_data_handlers/util.py#L875
)
```
step = (y1 - y0) / length
vals = np.around(np.arange(y0 + step, y1 + step, step))
```

Confirm that variable `vals` if of incorrect length
```
print(len(vals) == length)
> False
```

Alternatively, the code below produces similarly false results

```
from pydicom.pixel_data_handlers.util import _expand_segmented_lut 
lut = _expand_segmented_lut(([0, 1, 163, 1, 48, 255]), "B")
print(len(lut) == (1+48))
> False
```

`np.arange` [explicitly states](https://numpy.org/doc/stable/reference/generated/numpy.arange.html) that it's "results will often not be consistent" when using "non-integer step", which is a very possible scenario in this function. The following alternative code does function correctly:

```
vals = np.around(np.linspace(y0 + step, y1, length))
```

**Your environment**
```bash
$ python -m pydicom.env_info
module       | version
------       | -------
platform     | Darwin-20.5.0-x86_64-i386-64bit
Python       | 3.7.10 (default, Feb 26 2021, 10:16:00)  [Clang 10.0.0 ]
pydicom      | 2.1.2
gdcm         | _module not found_
jpeg_ls      | _module not found_
numpy        | 1.20.3
PIL          | 8.2.0
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1416?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) Report
> Merging [#1416](https://codecov.io/gh/pydicom/pydicom/pull/1416?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (a836e50) into [master](https://codecov.io/gh/pydicom/pydicom/commit/699c9f0a8e190d463dd828822106250523d38154?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (699c9f0) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/pydicom/pydicom/pull/1416/graphs/tree.svg?width=650&height=150&src=pr&token=YciLV15Lwr&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)](https://codecov.io/gh/pydicom/pydicom/pull/1416?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)

```diff
@@           Coverage Diff           @@
##           master    #1416   +/-   ##
=======================================
  Coverage   95.59%   95.59%           
=======================================
  Files          65       65           
  Lines       10203    10203           
=======================================
  Hits         9754     9754           
  Misses        449      449           
```


| [Impacted Files](https://codecov.io/gh/pydicom/pydicom/pull/1416?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) | Coverage Δ | |
|---|---|---|
| [pydicom/pixel\_data\_handlers/util.py](https://codecov.io/gh/pydicom/pydicom/pull/1416/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS9waXhlbF9kYXRhX2hhbmRsZXJzL3V0aWwucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1416?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1416?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom). Last update [699c9f0...a836e50](https://codecov.io/gh/pydicom/pydicom/pull/1416?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).

scaramallion: Could you add [test coverage](https://github.com/pydicom/pydicom/blob/master/pydicom/tests/test_handler_util.py#L1224) for the change please? The test should fail with the current code and pass with your PR.

And a line to the [release notes](https://github.com/pydicom/pydicom/blob/master/doc/release_notes/v2.2.0.rst)
thomsentner: Done
scaramallion: Looks good, thanks!

Patch:
diff --git a/doc/release_notes/v2.2.0.rst b/doc/release_notes/v2.2.0.rst
index 957ecf0c7..8c00259ed 100644
--- a/doc/release_notes/v2.2.0.rst
+++ b/doc/release_notes/v2.2.0.rst
@@ -107,3 +107,5 @@ Fixes
 * Allow to deepcopy a `~pydicom.dataset.FileDataset` object (:issue:`1147`)
 * Fixed elements with a VR of **OL**, **OD** and **OV** not being set correctly
   when an encoded backslash was part of the element value (:issue:`1412`)
+* Fixed expansion of linear segments with floating point steps in
+  segmented LUTs (:issue:`1415`)
diff --git a/pydicom/pixel_data_handlers/util.py b/pydicom/pixel_data_handlers/util.py
index afdb8cddb..d5e75071c 100644
--- a/pydicom/pixel_data_handlers/util.py
+++ b/pydicom/pixel_data_handlers/util.py
@@ -873,7 +873,7 @@ def _expand_segmented_lut(
                 lut.extend([y1] * length)
             else:
                 step = (y1 - y0) / length
-                vals = np.around(np.arange(y0 + step, y1 + step, step))
+                vals = np.around(np.linspace(y0 + step, y1, length))
                 lut.extend([int(vv) for vv in vals])
         elif opcode == 2:
             # C.7.9.2.3: Indirect segment


Test patch:
diff --git a/pydicom/tests/test_handler_util.py b/pydicom/tests/test_handler_util.py
index 01dbfd557..61cd64f15 100644
--- a/pydicom/tests/test_handler_util.py
+++ b/pydicom/tests/test_handler_util.py
@@ -1252,6 +1252,11 @@ class TestNumpy_ExpandSegmentedLUT:
         out = _expand_segmented_lut(data, 'H')
         assert [-400, -320, -240, -160, -80, 0] == out
 
+        # Positive slope, floating point steps
+        data = (0, 1, 163, 1, 48, 255)
+        out = _expand_segmented_lut(data, 'H')
+        assert (1 + 48) == len(out)
+
         # No slope
         data = (0, 2, 0, 28672, 1, 5, 28672)
         out = _expand_segmented_lut(data, 'H')