Skip to content

pointsetbase set get numpy arrays #5045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

thewtex
Copy link
Member

@thewtex thewtex commented Dec 11, 2024

  • STYLE: Use Python in Common python test names
  • ENH: Shaped NumPy array input to SetPointsByCoordinates

Follow the convention to end in "PythonTest" for python tests to help
identifying the tests.

We had, for example, two tests nameed itkPointSetTest (one C++ and one
Python).
Accept and n_points by dimension shaped NumPy array input to
GetPointsByCoordinates, which is the expected shape for point
coordinates. Continue to also support flat array inputs.

Also add a corresponding shaped NumPy output in a GetPointsByCoordinates method.

Add GetPointDimension() to PointSetBase. This provides access to the
point dimension at runtime for GetPointsByCoordinates and is similar to
GetImageDimension() on ImageBase.
@thewtex thewtex requested a review from N-Dekker December 11, 2024 19:24
@github-actions github-actions bot added type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots area:Python wrapping Python bindings for a class type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct area:Core Issues affecting the Core module labels Dec 11, 2024
Copy link
Member

@dzenanz dzenanz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@hjmjohnson hjmjohnson merged commit 1790947 into InsightSoftwareConsortium:master Dec 12, 2024
17 checks passed
@N-Dekker
Copy link
Contributor

N-Dekker commented Dec 12, 2024

@hjmjohnson Why is this PR merged so quickly, while my PR #4860 literally took months to get merged? I would have liked to get an opportunity to review this PR, especially because it adjusts the code that I originally contributed. Also, Matt assigned me as one of the reviewers.

Comment on lines +122 to +128
/** Point dimension. The dimension of a point is fixed at compile-time. */
static constexpr unsigned int
GetPointDimension()
{
return PointDimension;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This member function seems redundant, as PointDimension is already a public member. Are static constexpr public data members (like PointDimension) not made available already, by SWIG?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@N-Dekker correct -- Python only has access to the run-time methods.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Matt. It's a pity though, if SWIG cannot wrap them automatically. So just to be sure, I asked the question at swig/discussions as well:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a high level, as a runtime language, Python has access to runtime information.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reply from William S Fulton (@wsfulton) at swig/swig#3088 (comment) suggests that it is possible to directly get the value of a static constexpr data member of a template class, in Python. (So then the GetPointDimension() member function that you added should not be necessary.) However, I haven't yet been able to figure out how to make it work. 🤷 To be continued, hopefully...

Comment on lines -45 to -48
for i in range(number_of_points):
point = points.GetElement(i)
for j in range(dimension):
self.assertAlmostEqual(point[j], np_array[i * dimension + j])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good test for the flat array case. I think it should not have been removed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do retain testing for the flat array input. IT has the same output as the shaped array, and it is tested with the more detailed and efficient method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Matt, but in order to properly test for the flat array input, the asserts should be placed directly after the SetPointsByCoordinates(<flat array>) call. Not just after the SetPointsByCoordinates(<shaped array>) call.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also check the return value after passing the flat array: #5056

Comment on lines +701 to +703
def GetPointsByCoordinates(self):
"""Get the points of the pointset by providing their coordinates
as a NumPy array. The array will have a shape of (n_points, dimension)."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the name "GetPointsByCoordinates". Shouldn't it be something like "GetPointsAsNumpyArray"? Or "converts_points_to_np_array"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes most sense to retain parity of SetPointsByCoordinates with GetPointsByCoordinates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, but we could then also rename the new SetPointsByCoordinates. My original intention was to express that SetPointsByCoordinates sets the points by specifying a simple sequence of coordinates. For the newly added functionality, SetPointsByNumpyArray (or set_points_by_np_array) might be a more appropriate name, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the API getting unnecessarily complicated?

I thought that you discouraged the use of SetPoints?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the API getting unnecessarily complicated?

When two functions do different things, I think it is clearer to also use different names for those functions. The original SetPoints(PointsContainer *) (which has been there for many, many years already, including ITK 1, 2, 3, 4, 5, and 6 alpha 1) shares the container between the caller and the PointSet. The new SetPointsByCoordinates(const std::vector<CoordinateType> &) copies the coordinates from the caller into the container of the PointSet.

I thought that you discouraged the use of SetPoints?

The original SetPoints(PointsContainer *) is OK to me. It's OK to "share" your container with a point set, if that's what you want. However, the extra SetPoints(PointsVectorContainer *) overload that was added with ITK v5.3.0 appears problematic, as I explained at issue #4848 If I understand correctly, the extra SetPoints(PointsVectorContainer *) overload was specifically introduced so support flat coordinate arrays. But unfortunately it may lead to undefined behavior. The new SetPointsByCoordinates(const std::vector<CoordinateType> &) was just meant to provide a safe alternative to the problematic SetPoints(PointsVectorContainer *) overload of ITK >= v5.3.0.

Is that a helpful explanation to you?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@N-Dekker I understand what you mean. However, I do not see the need for another method, which over-complicates the API. It is standard practice in object-oriented APIs to have the same method accept two different types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:Core Issues affecting the Core module area:Python wrapping Python bindings for a class type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants