diff --git a/.docker/requirements.txt b/.docker/requirements.txt index f4aa52ee1..f1f39ef03 100644 --- a/.docker/requirements.txt +++ b/.docker/requirements.txt @@ -1,10 +1,12 @@ # This file is just used to get security alerts from GitHub. Make sure the versions match # in worker.dockerfile. -numpy==1.22.* +numpy==1.24.* opencv-contrib-python-headless==4.6.0 -scipy==1.10.0 -scikit-learn -matplotlib==3.5.2 +scipy==1.10.* +scikit-learn==1.2.* +matplotlib==3.6.* PyExcelerate==0.6.7 Pillow==10.2.0 Shapely==1.8.1 +torch==2.1.* +torchvision==0.16.* diff --git a/.docker/worker.dockerfile b/.docker/worker.dockerfile index 7f9eb9144..bac552ee9 100644 --- a/.docker/worker.dockerfile +++ b/.docker/worker.dockerfile @@ -1,32 +1,36 @@ -# PHP 8.1.13 -#FROM php:8.1-alpine -FROM php@sha256:f9e31f22bdd89c1334a03db5c8800a5f3b1e1fe042d470adccf58a29672c6202 +# PHP 8.1.27 +# FROM php:8.1 +FROM php@sha256:9b5dfb7deef3e48d67b2599e4d3967bb3ece19fd5ba09cb8e7ee10f5facf36e0 MAINTAINER Martin Zurowietz LABEL org.opencontainers.image.source https://github.com/biigle/core -ARG OPENCV_VERSION=4.6.0-r3 -RUN apk add --no-cache \ - eigen \ +RUN LC_ALL=C.UTF-8 apt-get update \ + && apt-get install -y --no-install-recommends \ ffmpeg \ - lapack \ - openblas \ - py3-numpy \ python3 \ - py3-opencv="$OPENCV_VERSION" + python3-numpy \ + python3-opencv \ + python3-scipy \ + python3-sklearn \ + python3-matplotlib \ + python3-shapely \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -r /var/lib/apt/lists/* RUN ln -s "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" ADD ".docker/all-php.ini" "$PHP_INI_DIR/conf.d/all.ini" -RUN apk add --no-cache \ - libxml2 \ - libzip \ - openssl \ - postgresql \ - && apk add --no-cache --virtual .build-deps \ +RUN LC_ALL=C.UTF-8 apt-get update \ + && apt-get install -y --no-install-recommends \ libxml2-dev \ libzip-dev \ - postgresql-dev \ - && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ + libpq-dev \ + && apt-get install -y --no-install-recommends \ + libxml2 \ + libzip4 \ + postgresql-client \ + && docker-php-ext-configure pgsql -with-pgsql=/usr/bin/pgsql \ && docker-php-ext-install -j$(nproc) \ exif \ pcntl \ @@ -35,15 +39,29 @@ RUN apk add --no-cache \ pgsql \ soap \ zip \ - && apk del --purge .build-deps + && apt-get purge -y \ + libxml2-dev \ + libzip-dev \ + libpq-dev \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -r /var/lib/apt/lists/* # Configure proxy if there is any. See: https://stackoverflow.com/a/2266500/1796523 RUN [ -z "$HTTP_PROXY" ] || pear config-set http_proxy $HTTP_PROXY -RUN apk add --no-cache yaml \ - && apk add --no-cache --virtual .build-deps g++ make autoconf yaml-dev \ + +RUN LC_ALL=C.UTF-8 apt-get update \ + && apt-get install -y --no-install-recommends \ + libyaml-dev \ + && apt-get install -y --no-install-recommends \ + libyaml-0-2 \ && pecl install yaml \ - && docker-php-ext-enable yaml \ - && apk del --purge .build-deps + && printf "\n" | docker-php-ext-enable yaml \ + && apt-get purge -y \ + libyaml-dev \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -r /var/lib/apt/lists/* ARG PHPREDIS_VERSION=5.3.7 RUN curl -L -o /tmp/redis.tar.gz https://github.com/phpredis/phpredis/archive/${PHPREDIS_VERSION}.tar.gz \ @@ -53,72 +71,38 @@ RUN curl -L -o /tmp/redis.tar.gz https://github.com/phpredis/phpredis/archive/${ && mv phpredis-${PHPREDIS_VERSION} /usr/src/php/ext/redis \ && docker-php-ext-install -j$(nproc) redis -ENV PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:${PKG_CONFIG_PATH}" -# Install vips from source because the apk package does not have dzsave support! Install -# libvips and the vips PHP extension in one go so the *-dev dependencies are reused. -ARG LIBVIPS_VERSION=8.12.2 -ARG PHP_VIPS_EXT_VERSION=1.0.13 -RUN apk add --no-cache --virtual .build-deps \ - autoconf \ - automake \ - build-base \ - expat-dev \ - glib-dev \ - libgsf-dev \ - libjpeg-turbo-dev \ - libpng-dev \ - tiff-dev \ - librsvg-dev \ - && apk add --no-cache \ - expat \ - glib \ - libgsf \ - libjpeg-turbo \ - libpng \ - tiff \ - librsvg \ - && cd /tmp \ - && curl -L https://github.com/libvips/libvips/releases/download/v${LIBVIPS_VERSION}/vips-${LIBVIPS_VERSION}.tar.gz -o vips-${LIBVIPS_VERSION}.tar.gz \ - && tar -xzf vips-${LIBVIPS_VERSION}.tar.gz \ - && cd vips-${LIBVIPS_VERSION} \ - && ./configure \ - --without-python \ - --enable-debug=no \ - --disable-dependency-tracking \ - --disable-static \ - && make -j $(nproc) \ - && make -s install-strip \ - && cd /tmp \ - && curl -L https://github.com/libvips/php-vips-ext/raw/master/vips-${PHP_VIPS_EXT_VERSION}.tgz -o vips-${PHP_VIPS_EXT_VERSION}.tgz \ - && echo '' | pecl install vips-${PHP_VIPS_EXT_VERSION}.tgz \ +# ENV PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:${PKG_CONFIG_PATH}" + +RUN LC_ALL=C.UTF-8 apt-get update \ + && apt-get install -y --no-install-recommends \ + libvips-dev \ + && apt-get install -y --no-install-recommends \ + libvips42 \ + && pecl install vips \ && docker-php-ext-enable vips \ - && rm -r /tmp/* \ - && apk del --purge .build-deps \ - && rm -rf /var/cache/apk/* + && apt-get purge -y \ + libvips-dev \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -r /var/lib/apt/lists/* # Unset proxy configuration again. RUN [ -z "$HTTP_PROXY" ] || pear config-set http_proxy "" -# Other Python dependencies are added with the OpenCV build above. -RUN apk add --no-cache py3-scipy py3-scikit-learn py3-matplotlib py3-shapely - -# Set this library path so the Python modules are linked correctly. -# See: https://github.com/python-pillow/Pillow/issues/1763#issuecomment-204252397 -ENV LIBRARY_PATH=/lib:/usr/lib -# Install Python dependencies. Note that these also depend on some image processing libs -# that were installed along with vips. -RUN apk add --no-cache --virtual .build-deps \ - python3-dev \ - py3-pip \ - py3-wheel \ - build-base \ - libjpeg-turbo-dev \ - libpng-dev \ - && pip3 install --no-cache-dir \ +RUN LC_ALL=C.UTF-8 apt-get update \ + && apt-get install -y --no-install-recommends \ + python3-pip \ + && pip3 install --no-cache-dir --break-system-packages \ PyExcelerate==0.6.7 \ Pillow==10.2.0 \ - && apk del --purge .build-deps \ - && rm -rf /var/cache/apk/* + && pip3 install --no-cache-dir --break-system-packages --index-url https://download.pytorch.org/whl/cpu \ + torch==2.1.* \ + torchvision==0.16.* \ + && apt-get purge -y \ + python3-pip \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -r /var/lib/apt/lists/* WORKDIR /var/www diff --git a/app/Events/AnnotationLabelAttached.php b/app/Events/AnnotationLabelAttached.php new file mode 100644 index 000000000..c710c9991 --- /dev/null +++ b/app/Events/AnnotationLabelAttached.php @@ -0,0 +1,17 @@ +annotation); + AnnotationLabelAttached::dispatch($annotationLabel); + return response($annotationLabel, 201); } catch (QueryException $e) { // Although we check for existence above, this error happened some time. diff --git a/app/Http/Controllers/Api/VideoAnnotationLabelController.php b/app/Http/Controllers/Api/VideoAnnotationLabelController.php index c949988c7..377e3be1b 100644 --- a/app/Http/Controllers/Api/VideoAnnotationLabelController.php +++ b/app/Http/Controllers/Api/VideoAnnotationLabelController.php @@ -2,6 +2,7 @@ namespace Biigle\Http\Controllers\Api; +use Biigle\Events\AnnotationLabelAttached; use Biigle\Http\Requests\DestroyVideoAnnotationLabel; use Biigle\Http\Requests\StoreVideoAnnotationLabel; use Biigle\VideoAnnotationLabel; @@ -65,6 +66,8 @@ public function store(StoreVideoAnnotationLabel $request) // should not be returned unset($annotationLabel->annotation); + AnnotationLabelAttached::dispatch($annotationLabel); + return response($annotationLabel, 201); } catch (QueryException $e) { // Although we check for existence above, this error happened some time. diff --git a/app/Jobs/CloneImagesOrVideos.php b/app/Jobs/CloneImagesOrVideos.php index 4f337e11a..bf58c1e5f 100644 --- a/app/Jobs/CloneImagesOrVideos.php +++ b/app/Jobs/CloneImagesOrVideos.php @@ -8,8 +8,8 @@ use Biigle\ImageAnnotation; use Biigle\ImageAnnotationLabel; use Biigle\ImageLabel; -use Biigle\Modules\Largo\Jobs\GenerateImageAnnotationPatch; -use Biigle\Modules\Largo\Jobs\GenerateVideoAnnotationPatch; +use Biigle\Modules\Largo\Jobs\ProcessAnnotatedImage; +use Biigle\Modules\Largo\Jobs\ProcessAnnotatedVideo; use Biigle\Project; use Biigle\Traits\ChecksMetadataStrings; use Biigle\Video; @@ -162,24 +162,26 @@ public function postProcessCloning($volume) { ProcessNewVolumeFiles::dispatch($volume); - if (class_exists(GenerateImageAnnotationPatch::class)) { - ImageAnnotation::join('images', 'images.id', '=', 'image_annotations.image_id') - ->where('images.volume_id', "=", $volume->id) - ->select('image_annotations.id') - ->eachById(function ($annotation) { - GenerateImageAnnotationPatch::dispatch($annotation) + // Give the ProcessNewVolumeFiles job a head start so the file thumbnails are + // generated (mostly) before the annotation thumbnails. + $delay = now()->addSeconds(30); + + if (class_exists(ProcessAnnotatedImage::class)) { + $volume->images()->whereHas('annotations') + ->eachById(function ($image) use ($delay) { + ProcessAnnotatedImage::dispatch($image) + ->delay($delay) ->onQueue(config('largo.generate_annotation_patch_queue')); - }, 1000, 'image_annotations.id', 'id'); + }); } - if (class_exists(GenerateVideoAnnotationPatch::class)) { - VideoAnnotation::join('videos', 'videos.id', '=', 'video_annotations.video_id') - ->where('videos.volume_id', "=", $volume->id) - ->select('video_annotations.id') - ->eachById(function ($annotation) { - GenerateVideoAnnotationPatch::dispatch($annotation) + if (class_exists(ProcessAnnotatedVideo::class)) { + $volume->videos() + ->whereHas('annotations')->eachById(function ($video) use ($delay) { + ProcessAnnotatedVideo::dispatch($video) + ->delay($delay) ->onQueue(config('largo.generate_annotation_patch_queue')); - }, 1000, 'video_annotations.id', 'id'); + }); } } diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6370bda9c..000000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -# This file is just used to get security alerts from GitHub. Make sure the versions match -# in .docker/worker.dockerfile. -numpy==1.22.* -opencv-contrib-python-headless==4.6.0 -scipy==1.10.0 -scikit-learn -matplotlib==3.5.2 -PyExcelerate==0.6.7 -Pillow==10.2.0 -Shapely==1.8.1 diff --git a/tests/php/Http/Controllers/Api/ImageAnnotationLabelControllerTest.php b/tests/php/Http/Controllers/Api/ImageAnnotationLabelControllerTest.php index 1f40b3137..dba26c170 100644 --- a/tests/php/Http/Controllers/Api/ImageAnnotationLabelControllerTest.php +++ b/tests/php/Http/Controllers/Api/ImageAnnotationLabelControllerTest.php @@ -3,11 +3,13 @@ namespace Biigle\Tests\Http\Controllers\Api; use ApiTestCase; +use Biigle\Events\AnnotationLabelAttached; use Biigle\Tests\AnnotationSessionTest; use Biigle\Tests\ImageAnnotationLabelTest; use Biigle\Tests\ImageAnnotationTest; use Cache; use Carbon\Carbon; +use Illuminate\Support\Facades\Event; use Session; class ImageAnnotationLabelControllerTest extends ApiTestCase @@ -115,6 +117,7 @@ public function testStoreLegacy() public function store($url) { + Event::fake(); $id = $this->annotation->id; $this->doTestApiRoute('POST', "{$url}/{$id}/labels"); @@ -150,12 +153,16 @@ public function store($url) $response->assertStatus(201); $this->assertEquals(1, $this->annotation->labels()->count()); + Event::assertDispatched(AnnotationLabelAttached::class); + $this->beAdmin(); $response = $this->json('POST', "{$url}/{$id}/labels", [ 'label_id' => $this->labelRoot()->id, 'confidence' => 0.1, ]); $response->assertStatus(201); + + Event::assertDispatched(AnnotationLabelAttached::class); $this->assertEquals(2, $this->annotation->labels()->count()); $response->assertJsonFragment([ 'id' => $this->labelRoot()->id, diff --git a/tests/php/Http/Controllers/Api/VideoAnnotationLabelControllerTest.php b/tests/php/Http/Controllers/Api/VideoAnnotationLabelControllerTest.php index 055e8f1b9..353d7e04d 100644 --- a/tests/php/Http/Controllers/Api/VideoAnnotationLabelControllerTest.php +++ b/tests/php/Http/Controllers/Api/VideoAnnotationLabelControllerTest.php @@ -3,11 +3,13 @@ namespace Biigle\Tests\Http\Controllers\Api; use ApiTestCase; +use Biigle\Events\AnnotationLabelAttached; use Biigle\MediaType; use Biigle\Tests\LabelTest; use Biigle\Tests\VideoAnnotationLabelTest; use Biigle\Tests\VideoAnnotationTest; use Biigle\Tests\VideoTest; +use Illuminate\Support\Facades\Event; class VideoAnnotationLabelControllerTest extends ApiTestCase { @@ -20,6 +22,7 @@ public function setUp(): void public function testStore() { + Event::fake(); $annotation = VideoAnnotationTest::create(['video_id' => $this->video->id]); $id = $annotation->id; @@ -51,6 +54,7 @@ public function testStore() $this->assertNotNull($label); $this->assertEquals($this->labelRoot()->id, $label->label_id); $this->assertEquals($this->editor()->id, $label->user_id); + Event::assertDispatched(AnnotationLabelAttached::class); $this ->postJson("api/v1/video-annotations/{$id}/labels", [ diff --git a/tests/php/Jobs/CloneImagesOrVideosTest.php b/tests/php/Jobs/CloneImagesOrVideosTest.php index f53016c06..568bdd8f0 100644 --- a/tests/php/Jobs/CloneImagesOrVideosTest.php +++ b/tests/php/Jobs/CloneImagesOrVideosTest.php @@ -5,8 +5,8 @@ use Biigle\Jobs\CloneImagesOrVideos; use Biigle\Jobs\ProcessNewVolumeFiles; use Biigle\MediaType; -use Biigle\Modules\Largo\Jobs\GenerateImageAnnotationPatch; -use Biigle\Modules\Largo\Jobs\GenerateVideoAnnotationPatch; +use Biigle\Modules\Largo\Jobs\ProcessAnnotatedImage; +use Biigle\Modules\Largo\Jobs\ProcessAnnotatedVideo; use Biigle\Tests\ImageAnnotationLabelTest; use Biigle\Tests\ImageAnnotationTest; use Biigle\Tests\ImageLabelTest; @@ -624,13 +624,13 @@ public function testHandleVolumeImages() (new CloneImagesOrVideos($request, $copy))->handle(); Queue::assertPushed(ProcessNewVolumeFiles::class); - Queue::assertNotPushed(GenerateImageAnnotationPatch::class); + Queue::assertNotPushed(ProcessAnnotatedImage::class); } public function testHandleImageAnnotationPatches() { - if (!class_exists(GenerateImageAnnotationPatch::class)) { - $this->markTestSkipped('Requires '.GenerateImageAnnotationPatch::class); + if (!class_exists(ProcessAnnotatedImage::class)) { + $this->markTestSkipped('Requires '.ProcessAnnotatedImage::class); } // The target project. @@ -655,15 +655,15 @@ public function testHandleImageAnnotationPatches() ]); (new CloneImagesOrVideos($request, $copy))->handle(); - // One job for the creation of the annotation and one job for GenerateImageAnnotationPatch + // One job for the creation of the annotation and one job for ProcessAnnotatedImage Queue::assertPushed(ProcessNewVolumeFiles::class); - Queue::assertPushed(GenerateImageAnnotationPatch::class); + Queue::assertPushed(ProcessAnnotatedImage::class); } public function testHandleVideoAnnotationPatches() { - if (!class_exists(GenerateVideoAnnotationPatch::class)) { - $this->markTestSkipped('Requires '.GenerateVideoAnnotationPatch::class); + if (!class_exists(ProcessAnnotatedVideo::class)) { + $this->markTestSkipped('Requires '.ProcessAnnotatedVideo::class); } // The target project. @@ -688,8 +688,8 @@ public function testHandleVideoAnnotationPatches() ]); (new CloneImagesOrVideos($request, $copy))->handle(); - // One job for the creation of the annotation and one job for GenerateVideoAnnotationPatch + // One job for the creation of the annotation and one job for ProcessAnnotatedVideo Queue::assertPushed(ProcessNewVolumeFiles::class); - Queue::assertPushed(GenerateVideoAnnotationPatch::class); + Queue::assertPushed(ProcessAnnotatedVideo::class); } }