diff --git a/LocCorr_new/CMakeLists.txt b/LocCorr_new/CMakeLists.txt index 00ddf1a..de3030b 100644 --- a/LocCorr_new/CMakeLists.txt +++ b/LocCorr_new/CMakeLists.txt @@ -19,6 +19,7 @@ option(DEBUG "Compile in debug mode" OFF) option(BASLER "Add Basler cameras support" OFF) option(GRASSHOPPER "Add GrassHopper cameras support" OFF) option(HIKROBOT "Add HikRobot cameras support" OFF) +option(TOUPCAM "Add Toupcam CMOS support" OFF) # default flags (c17 because MVS code have `typedef char bool`) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -W -Wextra -Werror -std=c17") @@ -63,6 +64,13 @@ if(HIKROBOT) else() list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/hikrobot.c") endif() +if(TOUPCAM) + pkg_check_modules(TOUPCAM REQUIRED toupcam) + add_definitions("-DTOUPCAM_FOUND=1") +else() + list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/toupcam.c") +endif() + pkg_check_modules(MODULES REQUIRED ${MODULES}) include(FindOpenMP) @@ -89,10 +97,10 @@ message("Install dir prefix: ${CMAKE_INSTALL_PREFIX}") # exe file add_executable(${PROJ} ${SOURCES}) # -I -target_include_directories(${PROJ} PUBLIC ${MODULES_INCLUDE_DIRS} ${FLYCAP_INCLUDE_DIRS} ${BASLER_INCLUDE_DIRS} ${MVS_INCLUDE_DIRS}) +target_include_directories(${PROJ} PUBLIC ${MODULES_INCLUDE_DIRS} ${FLYCAP_INCLUDE_DIRS} ${BASLER_INCLUDE_DIRS} ${MVS_INCLUDE_DIRS} ${TOUPCAM_INCLUDE_DIRS}) # -L -target_link_directories(${PROJ} PUBLIC ${MODULES_LIBRARY_DIRS} ${FLYCAP_LIBRARY_DIRS} ${BASLER_LIBRARY_DIRS} ${MVS_LIBRARY_DIRS}) -message("MOD: ${MODULES_LIBRARY_DIRS}, FC: ${FLYCAP_LIBRARY_DIRS}, MVS: ${MVS_LIBRARY_DIRS}") +target_link_directories(${PROJ} PUBLIC ${MODULES_LIBRARY_DIRS} ${FLYCAP_LIBRARY_DIRS} ${BASLER_LIBRARY_DIRS} ${MVS_LIBRARY_DIRS} ${TOUPCAM_LIBRARY_DIRS}) +message("MOD: ${MODULES_LIBRARY_DIRS}, FC: ${FLYCAP_LIBRARY_DIRS}, MVS: ${MVS_LIBRARY_DIRS}, TOUPCAM: ${TOUPCAM_LIBRARY_DIRS}") # -D add_definitions(${CFLAGS} -D_XOPEN_SOURCE=666 -D_POSIX_C_SOURCE=600700 -D_DEFAULT_SOURCE -DLOCALEDIR=\"${LOCALEDIR}\" -DPACKAGE_VERSION=\"${VERSION}\" -DGETTEXT_PACKAGE=\"${PROJ}\" @@ -100,7 +108,7 @@ add_definitions(${CFLAGS} -D_XOPEN_SOURCE=666 -D_POSIX_C_SOURCE=600700 -D_DEFAUL -DMAJOR_VERSION=\"${MAJOR_VERSION}\" -DTHREAD_NUMBER=${PROCESSOR_COUNT}) # -l -target_link_libraries(${PROJ} ${MODULES_LIBRARIES} ${FLYCAP_LIBRARIES} ${BASLER_LIBRARIES} ${MVS_LIBRARIES} -lm) +target_link_libraries(${PROJ} ${MODULES_LIBRARIES} ${FLYCAP_LIBRARIES} ${BASLER_LIBRARIES} ${MVS_LIBRARIES} ${TOUPCAM_LIBRARIES} -lm) # Installation of the program INSTALL(TARGETS ${PROJ} DESTINATION "bin") diff --git a/LocCorr_new/Toupcam.h b/LocCorr_new/Toupcam.h new file mode 100644 index 0000000..9889b4e --- /dev/null +++ b/LocCorr_new/Toupcam.h @@ -0,0 +1,28 @@ +/* + * This file is part of the loccorr project. + * Copyright 2026 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#ifdef TOUPCAM_FOUND +#include "cameracapture.h" // `camera` + +#define TOUPCAM_CAPT_NAME "toupcam" + +extern camera Toupcam; + +#endif diff --git a/LocCorr_new/cameracapture.c b/LocCorr_new/cameracapture.c index fa22143..69196de 100644 --- a/LocCorr_new/cameracapture.c +++ b/LocCorr_new/cameracapture.c @@ -239,12 +239,12 @@ static void *procthread(void* v){ void (*process)(Image*) = (procfn_t)v; #ifdef EBUG double t0 = sl_dtime(); + int imno = 0; #endif while(!stopwork){ pthread_mutex_lock(&capt_mutex); - //DBG("===== iCaptured=%d", iCaptured); if(Icap[iCaptured]){ - DBG("===== got image iCaptured=#%d @ %g", iCaptured, sl_dtime() - t0); + DBG("===== got image #%d, iCaptured=#%d @ %g", imno++, iCaptured, sl_dtime() - t0); Image *oIma = Icap[iCaptured]; // take image here and free buffer Icap[iCaptured] = NULL; pthread_mutex_unlock(&capt_mutex); @@ -300,6 +300,9 @@ int camcapture(void (*process)(Image*)){ ERR("pthread_create()"); } exptime = theconf.exptime; +#ifdef EBUG + static int imno = 0; +#endif while(1){ #ifdef EBUG double t0 = sl_dtime(); @@ -371,7 +374,7 @@ int camcapture(void (*process)(Image*)){ } continue; }else errctr = 0; - DBG("---- Grabbed @ %g", sl_dtime() - t0); + DBG("---- Grabbed #%d @ %g", imno++, sl_dtime() - t0); pthread_mutex_lock(&capt_mutex); if(iCaptured < 0) iCaptured = 0; else iCaptured = !iCaptured; @@ -388,7 +391,7 @@ int camcapture(void (*process)(Image*)){ Image_free(&oIma); } pthread_mutex_unlock(&capt_mutex); - DBG("unlocked, T=%g", sl_dtime() - t0); + DBG("T=%g", sl_dtime() - t0); } pthread_cancel(proc_thread); if(oIma) Image_free(&oIma); diff --git a/LocCorr_new/cmdlnopts.c b/LocCorr_new/cmdlnopts.c index 323d032..7943c88 100644 --- a/LocCorr_new/cmdlnopts.c +++ b/LocCorr_new/cmdlnopts.c @@ -70,7 +70,7 @@ static sl_option_t cmdlnopts[] = { {"logfile", NEED_ARG, NULL, 'l', arg_string, APTR(&G.logfile), _("file to save logs (default: none)")}, {"pidfile", NEED_ARG, NULL, 'P', arg_string, APTR(&G.pidfile), _("pidfile (default: " DEFAULT_PIDFILE ")")}, {"verbose", NO_ARGS, NULL, 'v', arg_none, APTR(&G.verb), _("increase verbosity level of log file (each -v increased by 1)")}, - {"input", NEED_ARG, NULL, 'i', arg_string, APTR(&G.inputname), _("file or directory name for monitoring (or grasshopper/basler/hikrobot for capturing)")}, + {"input", NEED_ARG, NULL, 'i', arg_string, APTR(&G.inputname), _("file or directory name for monitoring (or grasshopper/basler/hikrobot/toupcam for capturing)")}, {"blackp", NEED_ARG, NULL, 'b', arg_double, APTR(&G.throwpart), _("fraction of black pixels to throw away when make histogram eq")}, // {"radius", NEED_ARG, NULL, 'r', arg_int, APTR(&G.medradius), _("radius of median filter (r=1 -> 3x3, r=2 -> 5x5 etc.)")}, {"equalize", NO_ARGS, NULL, 'e', arg_int, APTR(&G.equalize), _("make historam equalization of saved jpeg")}, diff --git a/LocCorr_new/imagefile.c b/LocCorr_new/imagefile.c index 816368b..2bc3162 100644 --- a/LocCorr_new/imagefile.c +++ b/LocCorr_new/imagefile.c @@ -37,6 +37,7 @@ #include "hikrobot.h" #include "imagefile.h" #include "median.h" +#include "Toupcam.h" typedef struct{ const uint8_t signature[8]; @@ -112,6 +113,9 @@ InputType chkinput(const char *name){ #endif #ifdef MVS_FOUND if(0 == strcmp(name, HIKROBOT_CAPT_NAME)) return T_CAPT_HIKROBOT; +#endif +#ifdef TOUPCAM_FOUND + if(0 == strcmp(name, TOUPCAM_CAPT_NAME)) return T_CAPT_TOUPCAM; #endif struct stat fd_stat; stat(name, &fd_stat); diff --git a/LocCorr_new/imagefile.h b/LocCorr_new/imagefile.h index c3680df..f5950fa 100644 --- a/LocCorr_new/imagefile.h +++ b/LocCorr_new/imagefile.h @@ -61,7 +61,8 @@ typedef enum{ T_PNG, T_CAPT_GRASSHOPPER, // capture grasshopper T_CAPT_BASLER, - T_CAPT_HIKROBOT + T_CAPT_HIKROBOT, + T_CAPT_TOUPCAM, } InputType; void Image_minmax(Image *I); diff --git a/LocCorr_new/improc.c b/LocCorr_new/improc.c index b58523b..4da28fd 100644 --- a/LocCorr_new/improc.c +++ b/LocCorr_new/improc.c @@ -35,6 +35,7 @@ #include "improc.h" #include "inotify.h" #include "steppers.h" +#include "Toupcam.h" volatile atomic_ullong ImNumber = 0; // GLOBAL: counter of processed images volatile atomic_bool stopwork = FALSE; // GLOBAL: suicide @@ -424,7 +425,7 @@ int process_input(InputType tp, char *name){ if(tp == T_DIRECTORY){ imagedata = watchdr; return watch_directory(name, process_file); - }else if(tp == T_CAPT_GRASSHOPPER || tp == T_CAPT_BASLER || tp == T_CAPT_HIKROBOT){ + }else if(tp == T_CAPT_GRASSHOPPER || tp == T_CAPT_BASLER || tp == T_CAPT_HIKROBOT || tp == T_CAPT_TOUPCAM){ camera *cam = NULL; switch(tp){ case T_CAPT_GRASSHOPPER: @@ -440,6 +441,11 @@ int process_input(InputType tp, char *name){ case T_CAPT_HIKROBOT: #ifdef MVS_FOUND cam = &Hikrobot; +#endif + break; + case T_CAPT_TOUPCAM: +#ifdef TOUPCAM_FOUND + cam = &Toupcam; #endif break; default: return FALSE; diff --git a/LocCorr_new/main.c b/LocCorr_new/main.c index 53c3932..42340e8 100644 --- a/LocCorr_new/main.c +++ b/LocCorr_new/main.c @@ -76,7 +76,8 @@ static InputType chk_inp(const char *name){ if(!name) ERRX("Point file or directory name to monitor"); InputType itp = chkinput(name); if(T_WRONG == itp) return T_WRONG; - green("\n%s is a ", name); + green("\n%s", name); + printf(" is a "); switch(itp){ case T_DIRECTORY: printf("directory"); @@ -108,6 +109,9 @@ static InputType chk_inp(const char *name){ case T_CAPT_HIKROBOT: printf("hikrobot camera capture"); break; + case T_CAPT_TOUPCAM: + printf("toupcam camera capture"); + break; default: printf("unsupported type\n"); return T_WRONG; diff --git a/LocCorr_new/toupcam.c b/LocCorr_new/toupcam.c new file mode 100644 index 0000000..9c4ed6d --- /dev/null +++ b/LocCorr_new/toupcam.c @@ -0,0 +1,318 @@ +/* + * This file is part of the loccorr project. + * Copyright 2026 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include "Toupcam.h" + +// flags for image processing +typedef enum{ + IM_SLEEP, + IM_STARTED, + IM_READY, + IM_ERROR +} imstate_t; + +// devices +static ToupcamDeviceV2 g_dev[TOUPCAM_MAX] = {0}; +static struct{ + ToupcamDeviceV2* dev; // device + HToupcam hcam; // hcam for all functions + unsigned long long flags; // flags (read on connect) + pthread_mutex_t mutex; // lock mutex for `data` writing/reading + void* data; // image data + size_t imsz; // size of current image in bytes + imstate_t state; // current state + uint64_t imseqno; // number of image from connection + uint64_t lastcapno; // last captured image number +} toupcam = {0}; + +// array - max ROI; geometry - current ROI +static frameformat array = {.xoff=0, .yoff = 0, .w = 800, .h = 600}, geometry = {0}; + +// exptime (in seconds!!!) and starting of exposition +static double exptimeS = 0.1, starttime = 0.; + +#define TCHECK() do{if(!toupcam.hcam) return FALSE;}while(0) + +// return constant string with error code description +static const char *errcode(int ecode){ + switch(ecode){ + case 0: return "S_OK"; + case 1: return "S_FALSE"; + case 0x8000ffff: return "E_UNEXPECTED"; + case 0x80004001: return "E_NOTIMPL"; + case 0x80004002: return "E_NOINTERFACE"; + case 0x80070005: return "E_ACCESSDENIED"; + case 0x8007000e: return "E_OUTOFMEMORY"; + case 0x80070057: return "E_INVALIDARG"; + case 0x80004003: return "E_POINTER"; + case 0x80004005: return "E_FAIL"; + case 0x8001010e: return "E_WRONG_THREAD"; + case 0x8007001f: return "E_GEN_FAILURE"; + case 0x800700aa: return "E_BUSY"; + case 0x8000000a: return "E_PENDING"; + case 0x8001011f: return "E_TIMEOUT"; + case 0x80072743: return "E_UNREACH"; + default: return "Unknown error"; + } +} + +static void camcancel(){ + FNAME(); + if(!toupcam.hcam) return; + int e = Toupcam_Trigger(toupcam.hcam, 0); // stop triggering + if(e < 0) WARNX("Can't trigger 0: %s", errcode(e)); + e = Toupcam_Stop(toupcam.hcam); + if(e < 0) WARNX("Can't stop: %s", errcode(e)); + toupcam.state = IM_SLEEP; +} + +static void Tdisconnect(){ + FNAME(); + camcancel(); + if(toupcam.hcam){ + DBG("Close camera"); + Toupcam_Close(toupcam.hcam); + toupcam.hcam = NULL; + } + if(toupcam.data){ + DBG("Free image data"); + free(toupcam.data); + toupcam.data = NULL; + } +} + +static void EventCallback(unsigned nEvent, void _U_ *pCallbackCtx){ + FNAME(); + DBG("CALLBACK with evt %d", nEvent); + if(!toupcam.hcam || !toupcam.data){ DBG("NO data!"); return; } + if(nEvent != TOUPCAM_EVENT_IMAGE){ DBG("Not image event"); return; } + ToupcamFrameInfoV4 info = {0}; + pthread_mutex_lock(&toupcam.mutex); + if(Toupcam_PullImageV4(toupcam.hcam, toupcam.data, 0, 0, 0, &info) < 0){ + DBG("Error pulling image"); + toupcam.state = IM_ERROR; + }else{ + ++toupcam.imseqno; + DBG("Image %lu (%dx%d) ready!", toupcam.imseqno, info.v3.width, info.v3.height); + toupcam.state = IM_READY; + toupcam.imsz = info.v3.height * info.v3.width; + geometry.h = info.v3.height; + geometry.w = info.v3.width; + } + pthread_mutex_unlock(&toupcam.mutex); +} + +static int startexp(){ + FNAME(); + TCHECK(); + if(toupcam.state == IM_SLEEP){ + DBG("Sleeping -> start pull mode"); + if(Toupcam_StartPullModeWithCallback(toupcam.hcam, EventCallback, NULL) < 0){ + WARNX("Can't run PullMode with Callback!"); + return FALSE; + } + } + // Ask to trigger for several images (maximal speed available) + DBG("Trigger images"); + int e = Toupcam_Trigger(toupcam.hcam, 100); + if(e < 0){ + DBG("Can't ask for images stream: %s; try 1", errcode(e)); + e = Toupcam_Trigger(toupcam.hcam, 1); + if(e < 0){ + WARNX("Can't ask for next image: %s", errcode(e)); + return FALSE; + } + } + toupcam.state = IM_STARTED; + starttime = sl_dtime(); + return TRUE; +} + +static int Texp(float t){ // t - in milliseconds!!! + FNAME(); + TCHECK(); + if(t < FLT_EPSILON) return FALSE; + unsigned int microseconds = (unsigned)(t * 1e3f); + DBG("Set exptime to %dus", microseconds); + camcancel(); + int e = Toupcam_put_ExpoTime(toupcam.hcam, microseconds); + if(e < 0){ + WARNX("Can't set exp: %s", errcode(e)); + //startexp(); + return FALSE; + } + DBG("OK"); + if(Toupcam_get_ExpoTime(toupcam.hcam, µseconds) < 0) exptimeS = (double) t / 1e3; + else exptimeS = (float)microseconds / 1e6f; + DBG("Real exptime: %.4fs", exptimeS); + //startexp(); + return TRUE; +} + +static int Tconnect(){ + FNAME(); + Tdisconnect(); + unsigned N = Toupcam_EnumV2(g_dev); + if(0 == N){ + DBG("Found 0 toupcams"); + return FALSE; + } + toupcam.dev = &g_dev[0]; + toupcam.hcam = Toupcam_Open(g_dev[0].id); + if(!toupcam.hcam){ + WARN("Can't open toupcam camera"); + return FALSE; + } + DBG("Opened %s", toupcam.dev->displayname); + DBG("Clear ROI"); + Toupcam_put_Roi(toupcam.hcam, 0, 0, 0, 0); // clear ROI + // now fill camera geometry + unsigned int xoff, yoff, h, w; + DBG("Get ROI"); + Toupcam_get_Roi(toupcam.hcam, &xoff, &yoff, &w, &h); + DBG("off (x/y): %d/%d; wxh: %dx%d", xoff, yoff, w, h); + geometry.xoff = xoff; geometry.yoff = yoff; geometry.w = w; geometry.h = h; + toupcam.flags = Toupcam_query_Model(toupcam.hcam)->flag; + DBG("flags: 0x%llx", toupcam.flags); + DBG("Allocate data (%d bytes: 2*%d*%d)", 2 * array.w * array.h, array.w, array.h); + toupcam.data = calloc(array.w * array.h, 1); +#define OPT(opt, val, comment) do{DBG(comment); if(Toupcam_put_Option(toupcam.hcam, opt, val) < 0){ DBG("Can't put this option"); }}while(0) + // 12 frames/sec + OPT(TOUPCAM_OPTION_TRIGGER, 1, "Software/simulated trigger mode"); + OPT(TOUPCAM_OPTION_RAW, 1, "Put to RAW mode"); + OPT(TOUPCAM_OPTION_BINNING, 1, "Set binning to 1x1"); +#undef OPT + toupcam.state = IM_SLEEP; + toupcam.imseqno = toupcam.lastcapno = 0; + // 8bit + if(Toupcam_put_Option(toupcam.hcam, TOUPCAM_OPTION_BITDEPTH, 0) < 0) WARNX("Cant set bitdepth"); + if(Toupcam_put_Option(toupcam.hcam, TOUPCAM_OPTION_PIXEL_FORMAT, TOUPCAM_PIXELFORMAT_RAW8) < 0){ + WARNX("Cannot init 8bit mode!"); + Tdisconnect(); + return FALSE; + } + pthread_mutex_init(&toupcam.mutex, NULL); + if(!Texp(0.1)){ WARNX("Can't set default exptime"); } + return TRUE; +} + +static Image *Tcapture(){ + FNAME(); + if(!toupcam.hcam || !toupcam.data) return NULL; + if(!startexp()){ + WARNX("Can't start exposition"); + return NULL; + } + DBG("here, exptime=%gs, dstart=%g", exptimeS, (sl_dtime() - starttime)); + double tremain = 0.; + while(toupcam.state != IM_READY){ + tremain = exptimeS - (sl_dtime() - starttime); + if(tremain < -2.0){ + WARNX("Timeout - failed"); + camcancel(); + return NULL; + } + usleep(100); + } + if(toupcam.state != IM_READY){ + WARNX("State=%d, not ready", toupcam.state); + return NULL; + } + pthread_mutex_lock(&toupcam.mutex); + Image *o = u8toImage(toupcam.data, geometry.w, geometry.h, geometry.w); + toupcam.lastcapno = toupcam.imseqno; + pthread_mutex_unlock(&toupcam.mutex); + return o; +} + +static int Tbright(float b){ + FNAME(); + TCHECK(); + if(b < -255.f || b > 255.f){ + WARNX("Available brightness: -255..255"); + return FALSE; + } + int br = (int) b; + DBG("Try to set brightness to %d", br); + camcancel(); + int e = Toupcam_put_Brightness(toupcam.hcam, br); + //startexp(); + if(e < 0){ + WARNX("Can't set brightness: %s", errcode(e)); + return FALSE; + } + DBG("OK"); + return TRUE; +} + +static int Tgain(float g){ + FNAME(); + TCHECK(); + unsigned short G = (unsigned short)(100.f * g); + int ret = FALSE; + camcancel(); + if(Toupcam_put_ExpoAGain(toupcam.hcam, G) < 0){ + WARNX("Gain out of range: 1..8"); + }else{ DBG("GAIN is %d", G); ret = TRUE;} + //startexp(); + return ret; +} + +static float Tmaxgain(){ + FNAME(); + return 8.f; // toupcam SDK returns wrong value: 16. +} + +static int Tgeometry(frameformat *f){ + FNAME(); + TCHECK(); + int ret = FALSE; + camcancel(); + if(Toupcam_put_Roi(toupcam.hcam, (unsigned) f->xoff, (unsigned) f->yoff, (unsigned) f->w, (unsigned) f->h) >= 0){ + geometry = *f; + ret = TRUE; + } + //startexp(); + return ret; +} + +static int Tglimits(frameformat *max, frameformat *step){ + FNAME(); + TCHECK(); + if(max) *max = array; + if(step) *step = (frameformat){.w = 2, .h = 2, .xoff = 2, .yoff = 2}; + return TRUE; +} + +camera Toupcam = { + .disconnect = Tdisconnect, + .connect = Tconnect, + .capture = Tcapture, + .setbrightness = Tbright, + .setexp = Texp, + .setgain = Tgain, + .getmaxgain = Tmaxgain, + .setgeometry = Tgeometry, + .getgeomlimits = Tglimits, +};