-
Notifications
You must be signed in to change notification settings - Fork 1
Simplify ImgCreator by reducing use of NativeType and generic parameters
#14
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
base: main
Are you sure you want to change the base?
Conversation
It doesn't seem necessary - and it's harder to fulfil the requirements.
Attempts to make the class easier to use.
The type can be determined at the point the image is requested.
|
Upon reflection, I don’t think this PR goes far enough. It feels strange that we use a Because the public static ImgCreator fromServer(ImageServer server) {…}
public static ImgCreator fromServer(ImageServer server, CellCache cache) {…}There should be a way to query the type that will be used for an We could then consider adding separate methods such as public <T extends NativeType<T> & NumericType<T>> Img<T> createForLevel(int level, T type) {…}so that the type could explicitly be provided. If we do that, then we need to decide whether we permit lossless conversions (e.g. The most important distinction is likely between |
|
The whole point of the current design was to have a safe function that avoids these About defining the type in the builder vs defining it in the functions, since the type was entirely determined by the provided
What are the advantages of defining the type in the functions?
Having a builder makes the API more future-proof, but if we are sure that we won't add any new parameters, then yes we can go with
Did you try it in code? It's something I initially tried to do, but it wasn't working / needed a lot of unchecked casts.
But then doesn't it become quite similar to the code before this PR, with the same complexity on the calling side? |
Is that any better at runtime though? How would the user know which type to provide, unless they are essentially replicating every time what
That's ok for an example, but won't help us much in practice. In general we won't know what the type is for an image that hasn't been opened.
Yes, we should document the behavior. My last suggestion was to provide both options: where a type is provided, and where either isn't (and perhaps adding a specific alternative for ARGB, since it is a special case - and can be checked in a different way using public <T extends NumericType<T> & NativeType<T>> Img<T> createForLevel(int level) {
T type = getTypeOfServer(server);
return createForLevel(level, type);
}
public Img<ARGBType> createForLevelARGB(int level) {
var type = getTypeOfServer(server);
if (type instanceof ARGBType argb) {
return createForLevel(level, argb);
} else {
throw new IllegalArgumentException("Can't create ARGB Img from a non-RGB ImageServer");
}
}
public <T extends NumericType<T> & NativeType<T>> Img<T> createForLevel(int level, T type) {
if (level < 0 || level >= server.getMetadata().nLevels()) {
//.... Everything else
}
Do you already have ideas about additional parameters to add?
I think I'd need an example to understand what exactly isn't working.
Maybe... I think it has some advantages, but we probably need more code examples to investigate. My starting point was code related to thresholding that looked very repetitive. I wanted to shrink it, but I couldn't figure out a way. With this PR, I could reduce the number of lines to about 25% of the original length: https://github.com/qupath/qupath-extension-thresholding/pull/18#pullrequestreview-3479039144 I'm not convinced the PR is the right way to handle things, but it's such a core method any time we're using imglib2 that's worth trying to figure out how to make it as to call as possible - trying to minimise the need for duplication, generic gymnastics and unchecked exceptions elsewhere. |
The
They have two choices:
This was just to indicate that if you want to avoid
OK, but then if you want to avoid
No, we can use static methods.
On your PR, if I make var imageServer = ...
var type = ImgCreator.getTypeOfServer(imageServer);
var image = ImgCreator.builder(imageServer).build().createForLevel(0, type);because Would you use it differently?
You could shrink it by using the unsafe function. Here is an example: private static ImgLib2AutoThreshold<?> createImgLib2AutoThreshold(ImageServer<BufferedImage> server, double downsample, DimensionReduction dimensionReduction) {
if (server.isRGB()) {
if (dimensionReduction.changeType()) {
return new ImgLib2AutoThreshold<>(Converters.convert(
ImgCreator.builder(server, new ARGBType()).build().createForDownsample(downsample),
new RgbaReductionConverter<>(dimensionReduction),
new DoubleType()
));
} else {
return new ImgLib2AutoThreshold<>(Converters.convert(
ImgCreator.builder(server, new ARGBType()).build().createForDownsample(downsample),
new RgbaReductionConverter<>(dimensionReduction),
new UnsignedByteType()
));
}
} else {
return createRealTypeImgLib2AutoThreshold(server, downsample, dimensionReduction);
}
}
private static <T extends RealType<T> & NativeType<T>> ImgLib2AutoThreshold<?> createRealTypeImgLib2AutoThreshold(ImageServer<BufferedImage> server, double downsample, DimensionReduction dimensionReduction) {
RandomAccessibleInterval<T> accessible = (RandomAccessibleInterval<T>) ImgCreator.builder(server)
.build()
.createForDownsample(downsample);
if (dimensionReduction.changeType()) {
return new ImgLib2AutoThreshold<>(convertImage(accessible, accessible.getType().copy(), dimensionReduction));
} else {
return new ImgLib2AutoThreshold<>(convertImage(accessible, new DoubleType(), dimensionReduction));
}
}This is quite similar with what you suggested on https://github.com/qupath/qupath-extension-thresholding/pull/18#pullrequestreview-3479039144. |
|
To recap, because I think this is getting complicated:
|
|
Still thinking, so only a quick answer: instances of Perhaps List<Img<ByteType>> images = ImgCreator.builder(server)
.type(new ByteType()) // Optional, for safety
.cache(cache) // Optional
.buildForAllLevels() // Different build options according to what the user wants
RandomAccessibleInterval<?>> intervals = ImgCreator.builder(server)
.buildForDownsample(2.5) |
This tries to substantially reduce the complexity of
ImgCreator.The need arose when I was finding it really hard to manage generic parameters of the form
<T extends NativeType<T> & NumericType<T>>.Many methods don't/shouldn't care that we have a
NativeType, but rather only that we have aNumericType.After removing
NativeTypefrom some locations where it didn't seem needed, it became clear thatImgCreatordoesn't really need to store the type at all. Rather, the type can be determined from theImageServerat the point the image is requested.That means it's no longer necessary to provide a type to
ImgCreator.Builder- making it easier to call.Internally we do still need to always use a
NativeTypeso that we can create aLazyCellImage. This means we can retain the method signature:so calling code can be confident that the type is both a
NumericTypeand aNativeType- treating it as either.However I'm not sure if there are risks to this, which undermine compile-time checks. For example the following code looks ok in my IDE:
However it fails at runtime with
I'm not sure if this is a limitation of the way I'm using Java generics here (and we simply need to be checking the type that's returned), or if there's any way we can improve the API further. We would already have had a problem with the previous code if we passed the wrong type, so I don't think we're worse off here.