bytedeco / javacv-examples

Examples of using JavaCV / OpenCV library on Java Virtual Machine
341 stars 155 forks source link

Ex2ColorReduce report error #23

Closed before31 closed 2 years ago

before31 commented 2 years ago

In chapter02,Ex2ColorReduce,when I run the example code, I received this error: [info] running (fork) opencv_cookbook.chapter02.Ex2ColorReduce [error] Exception in thread "main" java.lang.IndexOutOfBoundsException: 518400 [error] at org.bytedeco.javacpp.indexer.Indexer.checkIndex(Indexer.java:101) [error] at org.bytedeco.javacpp.indexer.UByteRawIndexer.getRaw(UByteRawIndexer.java:73) [error] at org.bytedeco.javacpp.indexer.UByteRawIndexer.get(UByteRawIndexer.java:76) [error] at opencv_cookbook.chapter02.Ex2ColorReduce$.$anonfun$colorReduce$1(Ex2ColorReduce.scala:48) [error] at opencv_cookbook.chapter02.Ex2ColorReduce$.$anonfun$colorReduce$1$adapted(Ex2ColorReduce.scala:46) [error] at scala.collection.immutable.Range.foreach(Range.scala:190) [error] at opencv_cookbook.chapter02.Ex2ColorReduce$.colorReduce(Ex2ColorReduce.scala:46) [error] at opencv_cookbook.chapter02.Ex2ColorReduce$.delayedEndpoint$opencv_cookbook$chapter02$Ex2ColorReduce$1(Ex2ColorReduce.scala:28) [error] at opencv_cookbook.chapter02.Ex2ColorReduce$delayedInit$body.apply(Ex2ColorReduce.scala:22) [error] at scala.Function0.apply$mcV$sp(Function0.scala:39) [error] at scala.Function0.apply$mcV$sp$(Function0.scala:39) [error] at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17) [error] at scala.App.$anonfun$main$1(App.scala:76) [error] at scala.App.$anonfun$main$1$adapted(App.scala:76) [error] at scala.collection.IterableOnceOps.foreach(IterableOnce.scala:563) [error] at scala.collection.IterableOnceOps.foreach$(IterableOnce.scala:561) [error] at scala.collection.AbstractIterable.foreach(Iterable.scala:926) [error] at scala.App.main(App.scala:76) [error] at scala.App.main$(App.scala:74) [error] at opencv_cookbook.chapter02.Ex2ColorReduce$.main(Ex2ColorReduce.scala:22) [error] at opencv_cookbook.chapter02.Ex2ColorReduce.main(Ex2ColorReduce.scala)

jpsacha commented 2 years ago

For the reference here is the code that fails:

  def colorReduce(image: Mat, div: Int = 64): Mat = {

    val indexer = image.createIndexer().asInstanceOf[UByteIndexer]

    val nbElements = image.rows * image.cols * image.channels
    for (i <- 0 until nbElements) {
      val v = indexer.get(i) & 0xFF
      ...

It fails at indexer.get(i). Looks like regression in JavaCPP. It stopped working with JavaCPP 1.5.4 (Open CV 4.4). Last commit it still works: https://github.com/bytedeco/javacv-examples/commit/78ed75e8ac8294f4f8505c1fdc212f1106d38c78

Something changed with implementation of JavaCPP's indexer.get(i). In the past i=0 would get index 0, i=1 index 1, i=2 index 2 (figuratively speaking). You could go that way through all the pixels in the image. But now it jump by the stride, i=1 gets 1440, i=2 gets 2880, ... so it quickly causes IndexOutOfBoundsException. Here is a piece of code directly 'responsible' in StrideIndexer:

    @Override public long index(long i) {
        return i * strides[0];
    }

@saudet is there some change how the indexing is interpreted now? Is it a bug in the indexer?

saudet commented 2 years ago

I see, that was "fixed" for consistency when @matteodg refactored that part. What we need to do for linear indexing now is to reindex with the OneIndex, so something like this should work:

val indexer = image.createIndexer().reindex(new OneIndex(nbElements));
jpsacha commented 2 years ago

This gets a bit ugly in Scala as it needs some casting and type hints to compile:

    val indexer =
      image
        .createIndexer()
        .asInstanceOf[Indexer] // cast needed to avoid: UByteRawIndexer cannot be cast to class scala.runtime.Nothing$
        .reindex[UByteIndexer](new OneIndex(nbElements))
saudet commented 2 years ago

I don't understand why Scala would trip on that. The reindex() method is part of the Indexer interface. Could you explain?

jpsacha commented 2 years ago

I do not understand the "why" details myself. Some compiler errors are just weird.

Lets go piece by piece. For instance, this will compile:

image.createIndexer()

But when you add assignment:

val indexer = image.createIndexer()

it will not compile with error: cast needed to avoid: UByteRawIndexer cannot be cast to class scala.runtime.Nothing$. This does not make much sense to me. Maybe a bug in Scala. Tried with 2.13.8 and 3.1.2. The same error. But if you add cast it will compile:

val indexer = image.createIndexer().asInstanceOf[Indexer]

You can also use more detailed type

val indexer = image.createIndexer().asInstanceOf[UByteIndexer]

The last thing, reindex needs type hint to compile: reindex[UByteIndexer]. This way we have the complete:

    val indexer =
      image
        .createIndexer()
        .asInstanceOf[Indexer] // cast needed to avoid: UByteRawIndexer cannot be cast to class scala.runtime.Nothing$
        .reindex[UByteIndexer](new OneIndex(nbElements))

Overall looks that Scala has problems deriving correct types from Java code and needs help to make it clear, maybe related to type erasure in Java.

I may try to post this issue on Scala forums, to get some more clarity what is going on, but that may not immediately help with the code here.

I updated the example to make it work. Maybe changing the code to avoid using OneIndex will help clarity but will require more nested loops - more boilerplate for better clarity :)

jpsacha commented 2 years ago

By the way, Java also has some problem with correct type derivation when when variable indexer type is not defined. This compiles

UByteIndexer indexer = image.createIndexer().reindex(new OneIndex(100));
indexer.get(0);

But if you skip the type and use use var

var indexer = image.createIndexer().reindex(new OneIndex(nbElements));
indexer.get(0);

you will get error

java: cannot find symbol
   symbol:   method get(int)
   location: variable indexer of type org.bytedeco.javacpp.indexer.Indexer
before31 commented 2 years ago

Thanks you two guys. The updated code works. btw, is there any way to get the integer value directly , rather than performing 'value & 0xFF' every time after i got the unsigned byte value ?

saudet commented 2 years ago

I updated the example to make it work. Maybe changing the code to avoid using OneIndex will help clarity but will require more nested loops - more boilerplate for better clarity :)

It's also possible to create a flat view of the Mat before creating an Indexer with something like this:

UByteIndexer indexer = image.reshape(1, nbElements).createIndexer();

By the way, Java also has some problem with correct type derivation when when variable indexer type is not defined.

No, it doesn't, it works fine for abstract methods of Indexer such as reindex() and getDouble(), which can be inferred at compile time, unlike the get() methods.

Thanks you two guys. The updated code works. btw, is there any way to get the integer value directly , rather than performing 'value & 0xFF' every time after i got the unsigned byte value ?

We don't need those 0xFF, UByteIndexer does that, see http://bytedeco.org/news/2014/12/23/third-release/

before31 commented 2 years ago

We don't need those 0xFF, UByteIndexer does that, see http://bytedeco.org/news/2014/12/23/third-release/

Sorry for not describing the question clearly. First, I saw the 0xFF from the Ex2ColorReduce example, as below:

def colorReduce(image: Mat, div: Int = 64): Mat = {

val indexer = image.createIndexer().asInstanceOf[UByteIndexer]

val nbElements = image.rows * image.cols * image.channels
for (i <- 0 until nbElements) {
  // Convert to integer, byte is treated as an unsigned value
  val v = indexer.get(i) & 0xFF
  ...

Is there a mistake? In fact, I tried to remove the 0xFF, and the code goes right.

In another scenario, I got the value through the following code (java):

BytePointer bytePointer = image.ptr(j); byte value = bytePointer.get(0);

and the value is not the correct integer value but the unsigned byte. I have to use 0xFF to convert it to integer. Is there any way to get the integer value directly through the BytePointer ?

saudet commented 2 years ago

and the value is not the correct integer value but the unsigned byte. I have to use 0xFF to convert it to integer. Is there any way to get the integer value directly through the BytePointer ?

It's easy enough to add getUnsigned()/putUnsigned() methods. Would you mind opening a pull request to that effect?

before31 commented 2 years ago

It's easy enough to add getUnsigned()/putUnsigned() methods. Would you mind opening a pull request to that effect?

Of course I will. Maybe I would do that in the next few days.